diff --git a/charts/formbricks/README.md b/charts/formbricks/README.md index acd2f2b38f..61d4ab0b58 100644 --- a/charts/formbricks/README.md +++ b/charts/formbricks/README.md @@ -54,6 +54,38 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features - Provide `CUBEJS_API_SECRET` through your existing secret management flow, such as the generated app secret override or `deployment.envFrom`. - Keep Hub enabled. Cube should point at the same feedback records database that Hub writes to, unless you intentionally split that storage. +## Hub worker and self-hosted embeddings + +The chart deploys Hub API and, by default, a `hub-worker` deployment. Hub API is insert-only for River jobs; webhook dispatch and embedding jobs are processed by `hub-worker`. + +Self-hosted embeddings are disabled by default. Set `hub.embeddings.enabled=true` to deploy an internal Hugging Face Text Embeddings Inference (TEI) service and wire Hub API plus Hub worker to it through the OpenAI-compatible endpoint added in Hub: + +```yaml +hub: + worker: + enabled: true + + embeddings: + enabled: true + model: google/embeddinggemma-300m + servedModelName: google/embeddinggemma-300m + huggingFace: + token: hf_... +``` + +The generated Hub embedding configuration is: + +- `EMBEDDING_PROVIDER=openai` +- `EMBEDDING_MODEL=` +- `EMBEDDING_BASE_URL=http://-hub-embeddings:8080/v1` +- `EMBEDDING_PROVIDER_API_KEY` from a dedicated embeddings Secret + +The TEI service is internal-only (`ClusterIP`) and not exposed through ingress. For gated models such as `google/embeddinggemma-300m`, provide `hub.embeddings.huggingFace.token` or set `hub.embeddings.huggingFace.existingSecret`. + +When TEI auth is enabled, configure the shared key through `hub.embeddings.auth.apiKey` or `hub.embeddings.auth.existingSecret`; the chart manages both TEI `API_KEY` and Hub `EMBEDDING_PROVIDER_API_KEY` from that source. + +Autoscaling is opt-in for Hub API, Hub worker, and the embeddings runtime. If you scale the embeddings runtime above one replica while persistence is enabled, the cache PVC must support `ReadWriteMany`; otherwise set `hub.embeddings.persistence.enabled=false` or provide a compatible `existingClaim`. + ## Values | Key | Type | Default | Description | @@ -139,7 +171,40 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features | externalSecret.secretStore.name | string | `"aws-secrets-manager"` | | | formbricks.publicUrl | string | `""` | | | formbricks.webappUrl | string | `""` | | +| hub.autoscaling.enabled | bool | `false` | | +| hub.autoscaling.maxReplicas | int | `3` | | +| hub.autoscaling.minReplicas | int | `1` | | | hub.enabled | bool | `true` | | +| hub.embeddings.auth.enabled | bool | `true` | | +| hub.embeddings.auth.existingSecret | string | `""` | | +| hub.embeddings.auth.secretKey | string | `"EMBEDDING_PROVIDER_API_KEY"` | | +| hub.embeddings.autoscaling.enabled | bool | `false` | | +| hub.embeddings.autoscaling.maxReplicas | int | `2` | | +| hub.embeddings.autoscaling.minReplicas | int | `1` | | +| hub.embeddings.baseUrl | string | `""` | Defaults to the internal TEI service URL ending in `/v1`. | +| hub.embeddings.enabled | bool | `false` | | +| hub.embeddings.huggingFace.existingSecret | string | `""` | | +| hub.embeddings.huggingFace.token | string | `""` | | +| hub.embeddings.huggingFace.tokenKey | string | `"HF_TOKEN"` | | +| hub.embeddings.image.pullPolicy | string | `"IfNotPresent"` | | +| hub.embeddings.image.repository | string | `"ghcr.io/huggingface/text-embeddings-inference"` | | +| hub.embeddings.image.tag | string | `"cpu-1.9"` | | +| hub.embeddings.maxConcurrent | string | `"5"` | | +| hub.embeddings.model | string | `"google/embeddinggemma-300m"` | | +| hub.embeddings.persistence.enabled | bool | `true` | | +| hub.embeddings.persistence.mountPath | string | `"/data"` | | +| hub.embeddings.persistence.size | string | `"10Gi"` | | +| hub.embeddings.pdb.enabled | bool | `false` | | +| hub.embeddings.port | int | `8080` | | +| hub.embeddings.prometheusPort | int | `9000` | | +| hub.embeddings.replicas | int | `1` | | +| hub.embeddings.resources.limits.memory | string | `"8Gi"` | | +| hub.embeddings.resources.requests.cpu | string | `"4"` | | +| hub.embeddings.resources.requests.memory | string | `"8Gi"` | | +| hub.embeddings.runtime | string | `"tei"` | | +| hub.embeddings.servedModelName | string | `""` | Defaults to `hub.embeddings.model`. | +| hub.embeddings.service.port | int | `8080` | | +| hub.embeddings.service.type | string | `"ClusterIP"` | | | hub.env | object | `{}` | | | hub.existingSecret | string | `""` | | | hub.image.digest | string | `"sha256:14db7b3d285b6e9165b55693f9b83d08beff840a255fd77dd12882ee0a62f5cb"` | When set, takes precedence over tag (immutable pin). | @@ -149,10 +214,21 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features | hub.migration.activeDeadlineSeconds | int | `900` | | | hub.migration.backoffLimit | int | `3` | | | hub.migration.ttlSecondsAfterFinished | int | `300` | | +| hub.pdb.enabled | bool | `false` | | | hub.replicas | int | `1` | | | hub.resources.limits.memory | string | `"512Mi"` | | | hub.resources.requests.cpu | string | `"100m"` | | | hub.resources.requests.memory | string | `"256Mi"` | | +| hub.worker.autoscaling.enabled | bool | `false` | | +| hub.worker.autoscaling.maxReplicas | int | `5` | | +| hub.worker.autoscaling.minReplicas | int | `1` | | +| hub.worker.enabled | bool | `true` | | +| hub.worker.env | object | `{}` | | +| hub.worker.pdb.enabled | bool | `false` | | +| hub.worker.replicas | int | `1` | | +| hub.worker.resources.limits.memory | string | `"512Mi"` | | +| hub.worker.resources.requests.cpu | string | `"100m"` | | +| hub.worker.resources.requests.memory | string | `"256Mi"` | | | ingress.annotations | object | `{}` | | | ingress.enabled | bool | `false` | | | ingress.hosts[0].host | string | `"k8s.formbricks.com"` | | diff --git a/charts/formbricks/templates/_helpers.tpl b/charts/formbricks/templates/_helpers.tpl index 1680915681..a78fc3f7d5 100644 --- a/charts/formbricks/templates/_helpers.tpl +++ b/charts/formbricks/templates/_helpers.tpl @@ -114,6 +114,105 @@ hub-worker) must use this helper so they cannot drift apart. {{- end -}} {{- end }} +{{/* +Hub worker resource name. +*/}} +{{- define "formbricks.hubWorkerName" -}} +{{- $base := include "formbricks.name" . | trunc 52 | trimSuffix "-" }} +{{- printf "%s-hub-worker" $base | trimSuffix "-" }} +{{- end }} + +{{/* +Hub embeddings runtime resource name. +*/}} +{{- define "formbricks.hubEmbeddingsName" -}} +{{- $base := include "formbricks.name" . | trunc 48 | trimSuffix "-" }} +{{- printf "%s-hub-embeddings" $base | trimSuffix "-" }} +{{- end }} + +{{/* +Secret used by Hub and the embeddings runtime for the embeddings API key. +*/}} +{{- define "formbricks.hubEmbeddingsSecretName" -}} +{{- default (printf "%s-secret" (include "formbricks.hubEmbeddingsName" .)) .Values.hub.embeddings.auth.existingSecret -}} +{{- end }} + +{{/* +Secret used by the embeddings runtime for Hugging Face access. +*/}} +{{- define "formbricks.hubEmbeddingsHuggingFaceSecretName" -}} +{{- default (include "formbricks.hubEmbeddingsSecretName" .) .Values.hub.embeddings.huggingFace.existingSecret -}} +{{- end }} + +{{/* +Model name Hub sends to the OpenAI-compatible embeddings endpoint. +*/}} +{{- define "formbricks.hubEmbeddingsServedModelName" -}} +{{- default .Values.hub.embeddings.model .Values.hub.embeddings.servedModelName -}} +{{- end }} + +{{/* +OpenAI-compatible embeddings base URL used by Hub. +*/}} +{{- define "formbricks.hubEmbeddingsBaseURL" -}} +{{- if .Values.hub.embeddings.baseUrl -}} +{{- .Values.hub.embeddings.baseUrl -}} +{{- else -}} +{{- printf "http://%s:%v/v1" (include "formbricks.hubEmbeddingsName" .) (.Values.hub.embeddings.service.port | default .Values.hub.embeddings.port) -}} +{{- end -}} +{{- end }} + +{{/* +Embedding API key value for the generated embeddings secret. +*/}} +{{- define "formbricks.hubEmbeddingsApiKey" -}} +{{- $secretName := include "formbricks.hubEmbeddingsSecretName" . }} +{{- $secretKey := .Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace $secretName) }} +{{- if and $secret (index $secret.data $secretKey) }} + {{- index $secret.data $secretKey | b64dec -}} +{{- else if .Values.hub.embeddings.auth.apiKey }} + {{- .Values.hub.embeddings.auth.apiKey -}} +{{- else }} + {{- randAlphaNum 32 -}} +{{- end -}} +{{- end }} + +{{/* +Shared Hub embedding env. These values are managed from hub.embeddings when the +self-hosted runtime is enabled so Hub API and Hub worker cannot drift. +*/}} +{{- define "formbricks.hubEmbeddingEnv" -}} +{{- $root := .root -}} +{{- if $root.Values.hub.embeddings.enabled }} +- name: EMBEDDING_PROVIDER + value: "openai" +- name: EMBEDDING_MODEL + value: {{ include "formbricks.hubEmbeddingsServedModelName" $root | quote }} +- name: EMBEDDING_BASE_URL + value: {{ include "formbricks.hubEmbeddingsBaseURL" $root | quote }} +- name: EMBEDDING_PROVIDER_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "formbricks.hubEmbeddingsSecretName" $root }} + key: {{ $root.Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }} +- name: EMBEDDING_MAX_CONCURRENT + value: {{ $root.Values.hub.embeddings.maxConcurrent | quote }} +- name: EMBEDDING_NORMALIZE + value: {{ $root.Values.hub.embeddings.normalize | quote }} +{{- end }} +{{- end }} + +{{/* +Returns true when an env var is managed by hub.embeddings and should not be rendered from hub.env/worker.env. +*/}} +{{- define "formbricks.hubEmbeddingEnvManaged" -}} +{{- $key := .key -}} +{{- if has $key (list "EMBEDDING_PROVIDER" "EMBEDDING_MODEL" "EMBEDDING_BASE_URL" "EMBEDDING_PROVIDER_API_KEY" "EMBEDDING_MAX_CONCURRENT" "EMBEDDING_NORMALIZE") -}} +true +{{- end -}} +{{- end }} + {{- define "formbricks.postgresAdminPassword" -}} {{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }} @@ -142,13 +241,13 @@ hub-worker) must use this helper so they cannot drift apart. {{- end -}} {{- end }} -{{- define "formbricks.cronSecret" -}} -{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }} -{{- if $secret }} - {{- index $secret.data "CRON_SECRET" | b64dec -}} -{{- else }} - {{- randAlphaNum 32 -}} -{{- end -}} +{{- define "formbricks.cronSecret" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }} +{{- if $secret }} + {{- index $secret.data "CRON_SECRET" | b64dec -}} +{{- else }} + {{- randAlphaNum 32 -}} +{{- end -}} {{- end }} {{- define "formbricks.encryptionKey" -}} diff --git a/charts/formbricks/templates/hub-deployment.yaml b/charts/formbricks/templates/hub-deployment.yaml index c79e0ff6a8..5269762a66 100644 --- a/charts/formbricks/templates/hub-deployment.yaml +++ b/charts/formbricks/templates/hub-deployment.yaml @@ -14,7 +14,9 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} spec: + {{- if not .Values.hub.autoscaling.enabled }} replicas: {{ .Values.hub.replicas | default 1 }} + {{- end }} selector: matchLabels: app.kubernetes.io/name: {{ include "formbricks.hubname" . }} @@ -66,10 +68,13 @@ spec: secretKeyRef: name: {{ include "formbricks.hubSecretName" . }} key: HUB_API_KEY + {{- include "formbricks.hubEmbeddingEnv" (dict "root" $ "env" .Values.hub.env) | nindent 12 }} {{- range $key, $value := .Values.hub.env }} + {{- if not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key))) }} - name: {{ $key }} value: {{ $value | quote }} {{- end }} + {{- end }} {{- if .Values.hub.resources }} resources: {{- toYaml .Values.hub.resources | nindent 12 }} diff --git a/charts/formbricks/templates/hub-embeddings-deployment.yaml b/charts/formbricks/templates/hub-embeddings-deployment.yaml new file mode 100644 index 0000000000..9a9173de83 --- /dev/null +++ b/charts/formbricks/templates/hub-embeddings-deployment.yaml @@ -0,0 +1,158 @@ +{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled }} +{{- $embeddingsReplicas := int (.Values.hub.embeddings.replicas | default 1) -}} +{{- $embeddingsMaxReplicas := int (.Values.hub.embeddings.autoscaling.maxReplicas | default 1) -}} +{{- if and .Values.hub.embeddings.persistence.enabled (not (has "ReadWriteMany" .Values.hub.embeddings.persistence.accessModes)) (or (gt $embeddingsReplicas 1) (and .Values.hub.embeddings.autoscaling.enabled (gt $embeddingsMaxReplicas 1))) }} + {{- fail "hub.embeddings persistence with multiple replicas requires persistence.accessModes to include ReadWriteMany, or set hub.embeddings.persistence.enabled=false/use a ReadWriteMany existingClaim" }} +{{- end }} +{{- if and .Values.hub.embeddings.auth.existingSecret .Values.hub.embeddings.huggingFace.token (not .Values.hub.embeddings.huggingFace.existingSecret) }} + {{- fail "hub.embeddings.huggingFace.token cannot be stored when hub.embeddings.auth.existingSecret is set; put HF_TOKEN in the existing auth secret or set hub.embeddings.huggingFace.existingSecret" }} +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "formbricks.hubEmbeddingsName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} +spec: + {{- if not .Values.hub.embeddings.autoscaling.enabled }} + replicas: {{ .Values.hub.embeddings.replicas | default 1 }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + {{- with .Values.hub.embeddings.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.embeddings.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.hub.embeddings.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.embeddings.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.embeddings.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.embeddings.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.embeddings.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.deployment.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }} + {{- end }} + containers: + - name: hub-embeddings + image: "{{ .Values.hub.embeddings.image.repository }}:{{ .Values.hub.embeddings.image.tag }}" + imagePullPolicy: {{ .Values.hub.embeddings.image.pullPolicy }} + {{- with .Values.hub.embeddings.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.hub.embeddings.args }} + args: + {{- toYaml .Values.hub.embeddings.args | nindent 12 }} + {{- else }} + args: + - --model-id + - {{ .Values.hub.embeddings.model | quote }} + - --port + - {{ .Values.hub.embeddings.port | quote }} + - --huggingface-hub-cache + - {{ .Values.hub.embeddings.persistence.mountPath | quote }} + - --served-model-name + - {{ include "formbricks.hubEmbeddingsServedModelName" . | quote }} + {{- with .Values.hub.embeddings.revision }} + - --revision + - {{ . | quote }} + {{- end }} + {{- with .Values.hub.embeddings.extraArgs }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.hub.embeddings.port }} + protocol: TCP + - name: metrics + containerPort: {{ .Values.hub.embeddings.prometheusPort }} + protocol: TCP + {{- if or .Values.hub.embeddings.auth.enabled .Values.hub.embeddings.huggingFace.existingSecret .Values.hub.embeddings.huggingFace.token (gt (len .Values.hub.embeddings.env) 0) }} + env: + {{- if .Values.hub.embeddings.auth.enabled }} + - name: API_KEY + valueFrom: + secretKeyRef: + name: {{ include "formbricks.hubEmbeddingsSecretName" . }} + key: {{ .Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }} + {{- end }} + {{- if or .Values.hub.embeddings.huggingFace.existingSecret .Values.hub.embeddings.huggingFace.token }} + - name: HF_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "formbricks.hubEmbeddingsHuggingFaceSecretName" . }} + key: {{ .Values.hub.embeddings.huggingFace.tokenKey | default "HF_TOKEN" }} + {{- end }} + {{- range $key, $value := .Values.hub.embeddings.env }} + {{- if not (or (and $.Values.hub.embeddings.auth.enabled (eq $key "API_KEY")) (and (or $.Values.hub.embeddings.huggingFace.existingSecret $.Values.hub.embeddings.huggingFace.token) (eq $key "HF_TOKEN"))) }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- with .Values.hub.embeddings.probes.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.hub.embeddings.probes.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.hub.embeddings.probes.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.hub.embeddings.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.hub.embeddings.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.hub.embeddings.persistence.enabled }} + volumeMounts: + - name: model-cache + mountPath: {{ .Values.hub.embeddings.persistence.mountPath }} + {{- end }} + {{- if .Values.hub.embeddings.persistence.enabled }} + volumes: + - name: model-cache + persistentVolumeClaim: + claimName: {{ default (include "formbricks.hubEmbeddingsName" .) .Values.hub.embeddings.persistence.existingClaim }} + {{- end }} +{{- end }} diff --git a/charts/formbricks/templates/hub-embeddings-pvc.yaml b/charts/formbricks/templates/hub-embeddings-pvc.yaml new file mode 100644 index 0000000000..f86504eb7d --- /dev/null +++ b/charts/formbricks/templates/hub-embeddings-pvc.yaml @@ -0,0 +1,23 @@ +{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled .Values.hub.embeddings.persistence.enabled (not .Values.hub.embeddings.persistence.existingClaim) }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "formbricks.hubEmbeddingsName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} +spec: + accessModes: + {{- toYaml .Values.hub.embeddings.persistence.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.hub.embeddings.persistence.size }} + {{- with .Values.hub.embeddings.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/formbricks/templates/hub-embeddings-secret.yaml b/charts/formbricks/templates/hub-embeddings-secret.yaml new file mode 100644 index 0000000000..0add354bf9 --- /dev/null +++ b/charts/formbricks/templates/hub-embeddings-secret.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled (not .Values.hub.embeddings.auth.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "formbricks.hubEmbeddingsSecretName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} +type: Opaque +data: + {{ .Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }}: {{ include "formbricks.hubEmbeddingsApiKey" . | b64enc }} + {{- if and (not .Values.hub.embeddings.huggingFace.existingSecret) .Values.hub.embeddings.huggingFace.token }} + {{ .Values.hub.embeddings.huggingFace.tokenKey | default "HF_TOKEN" }}: {{ .Values.hub.embeddings.huggingFace.token | b64enc }} + {{- end }} +{{- end }} diff --git a/charts/formbricks/templates/hub-embeddings-service.yaml b/charts/formbricks/templates/hub-embeddings-service.yaml new file mode 100644 index 0000000000..2b2e50f316 --- /dev/null +++ b/charts/formbricks/templates/hub-embeddings-service.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "formbricks.hubEmbeddingsName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.embeddings.service.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.embeddings.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.hub.embeddings.service.type }} + selector: + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.hub.embeddings.service.port }} + targetPort: http + protocol: TCP + - name: metrics + port: {{ .Values.hub.embeddings.prometheusPort }} + targetPort: metrics + protocol: TCP +{{- end }} diff --git a/charts/formbricks/templates/hub-hpa.yaml b/charts/formbricks/templates/hub-hpa.yaml new file mode 100644 index 0000000000..342fb9747d --- /dev/null +++ b/charts/formbricks/templates/hub-hpa.yaml @@ -0,0 +1,102 @@ +{{- if and .Values.hub.enabled .Values.hub.autoscaling.enabled }} +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "formbricks.hubname" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubname" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.autoscaling.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.autoscaling.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "formbricks.hubname" . }} + minReplicas: {{ .Values.hub.autoscaling.minReplicas }} + maxReplicas: {{ .Values.hub.autoscaling.maxReplicas }} + metrics: + {{- toYaml .Values.hub.autoscaling.metrics | nindent 4 }} + {{- with .Values.hub.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- if and .Values.hub.enabled .Values.hub.worker.enabled .Values.hub.worker.autoscaling.enabled }} +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "formbricks.hubWorkerName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-worker + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.worker.autoscaling.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.worker.autoscaling.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "formbricks.hubWorkerName" . }} + minReplicas: {{ .Values.hub.worker.autoscaling.minReplicas }} + maxReplicas: {{ .Values.hub.worker.autoscaling.maxReplicas }} + metrics: + {{- toYaml .Values.hub.worker.autoscaling.metrics | nindent 4 }} + {{- with .Values.hub.worker.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled .Values.hub.embeddings.autoscaling.enabled }} +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "formbricks.hubEmbeddingsName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.embeddings.autoscaling.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.embeddings.autoscaling.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "formbricks.hubEmbeddingsName" . }} + minReplicas: {{ .Values.hub.embeddings.autoscaling.minReplicas }} + maxReplicas: {{ .Values.hub.embeddings.autoscaling.maxReplicas }} + metrics: + {{- toYaml .Values.hub.embeddings.autoscaling.metrics | nindent 4 }} + {{- with .Values.hub.embeddings.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/formbricks/templates/hub-pdb.yaml b/charts/formbricks/templates/hub-pdb.yaml new file mode 100644 index 0000000000..4d3fda992a --- /dev/null +++ b/charts/formbricks/templates/hub-pdb.yaml @@ -0,0 +1,129 @@ +{{- if and .Values.hub.enabled .Values.hub.pdb.enabled }} +{{- $hasMinAvailable := not (kindIs "invalid" .Values.hub.pdb.minAvailable) -}} +{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.hub.pdb.maxUnavailable) -}} +{{- if and $hasMinAvailable $hasMaxUnavailable }} + {{- fail "hub.pdb.minAvailable and hub.pdb.maxUnavailable are mutually exclusive; set only one" }} +{{- end }} +{{- if not (or $hasMinAvailable $hasMaxUnavailable) }} + {{- fail "hub.pdb.enabled is true but neither hub.pdb.minAvailable nor hub.pdb.maxUnavailable is set; set exactly one" }} +{{- end }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "formbricks.hubname" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubname" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.pdb.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $hasMinAvailable }} + minAvailable: {{ .Values.hub.pdb.minAvailable }} + {{- end }} + {{- if $hasMaxUnavailable }} + maxUnavailable: {{ .Values.hub.pdb.maxUnavailable }} + {{- end }} + {{- with .Values.hub.pdb.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "formbricks.hubname" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +{{- if and .Values.hub.enabled .Values.hub.worker.enabled .Values.hub.worker.pdb.enabled }} +{{- $hasMinAvailable := not (kindIs "invalid" .Values.hub.worker.pdb.minAvailable) -}} +{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.hub.worker.pdb.maxUnavailable) -}} +{{- if and $hasMinAvailable $hasMaxUnavailable }} + {{- fail "hub.worker.pdb.minAvailable and hub.worker.pdb.maxUnavailable are mutually exclusive; set only one" }} +{{- end }} +{{- if not (or $hasMinAvailable $hasMaxUnavailable) }} + {{- fail "hub.worker.pdb.enabled is true but neither hub.worker.pdb.minAvailable nor hub.worker.pdb.maxUnavailable is set; set exactly one" }} +{{- end }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "formbricks.hubWorkerName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-worker + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.worker.pdb.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.worker.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $hasMinAvailable }} + minAvailable: {{ .Values.hub.worker.pdb.minAvailable }} + {{- end }} + {{- if $hasMaxUnavailable }} + maxUnavailable: {{ .Values.hub.worker.pdb.maxUnavailable }} + {{- end }} + {{- with .Values.hub.worker.pdb.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled .Values.hub.embeddings.pdb.enabled }} +{{- $hasMinAvailable := not (kindIs "invalid" .Values.hub.embeddings.pdb.minAvailable) -}} +{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.hub.embeddings.pdb.maxUnavailable) -}} +{{- if and $hasMinAvailable $hasMaxUnavailable }} + {{- fail "hub.embeddings.pdb.minAvailable and hub.embeddings.pdb.maxUnavailable are mutually exclusive; set only one" }} +{{- end }} +{{- if not (or $hasMinAvailable $hasMaxUnavailable) }} + {{- fail "hub.embeddings.pdb.enabled is true but neither hub.embeddings.pdb.minAvailable nor hub.embeddings.pdb.maxUnavailable is set; set exactly one" }} +{{- end }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "formbricks.hubEmbeddingsName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-embeddings + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} + {{- with .Values.hub.embeddings.pdb.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hub.embeddings.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $hasMinAvailable }} + minAvailable: {{ .Values.hub.embeddings.pdb.minAvailable }} + {{- end }} + {{- if $hasMaxUnavailable }} + maxUnavailable: {{ .Values.hub.embeddings.pdb.maxUnavailable }} + {{- end }} + {{- with .Values.hub.embeddings.pdb.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/formbricks/templates/hub-worker-deployment.yaml b/charts/formbricks/templates/hub-worker-deployment.yaml new file mode 100644 index 0000000000..9a807c34b7 --- /dev/null +++ b/charts/formbricks/templates/hub-worker-deployment.yaml @@ -0,0 +1,97 @@ +{{- if and .Values.hub.enabled .Values.hub.worker.enabled }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "formbricks.hubWorkerName" . }} + labels: + helm.sh/chart: {{ include "formbricks.chart" . }} + app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-worker + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} +spec: + {{- if not .Values.hub.worker.autoscaling.enabled }} + replicas: {{ .Values.hub.worker.replicas | default 1 }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: hub-worker + spec: + {{- with .Values.hub.worker.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.worker.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.worker.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hub.worker.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.deployment.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-hub-api + image: {{ include "formbricks.hubImage" . }} + imagePullPolicy: {{ .Values.hub.image.pullPolicy }} + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + command: + - sh + - -c + - | + until wget --no-verbose --tries=1 --spider http://{{ include "formbricks.hubname" . }}:8080/health; do + echo "Waiting for Hub API migrations and health check..." + sleep 5 + done + containers: + - name: hub-worker + image: {{ include "formbricks.hubImage" . }} + imagePullPolicy: {{ .Values.hub.image.pullPolicy }} + command: + - /app/hub-worker + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + envFrom: + - secretRef: + name: {{ include "formbricks.hubSecretName" . }} + {{- if or .Values.hub.embeddings.enabled (gt (len .Values.hub.env) 0) (gt (len .Values.hub.worker.env) 0) }} + env: + {{- $workerEnv := merge (dict) .Values.hub.env .Values.hub.worker.env }} + {{- include "formbricks.hubEmbeddingEnv" (dict "root" $ "env" $workerEnv) | nindent 12 }} + {{- range $key, $value := .Values.hub.env }} + {{- if and (not (hasKey $.Values.hub.worker.env $key)) (not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key)))) }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + {{- range $key, $value := .Values.hub.worker.env }} + {{- if not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key))) }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.hub.worker.resources }} + resources: + {{- toYaml .Values.hub.worker.resources | nindent 12 }} + {{- end }} +{{- end }} diff --git a/charts/formbricks/values.yaml b/charts/formbricks/values.yaml index 9a83d7ff67..38f4b119b7 100644 --- a/charts/formbricks/values.yaml +++ b/charts/formbricks/values.yaml @@ -588,6 +588,241 @@ hub: # Optional env vars (non-secret). Use existingSecret for secret values such as DATABASE_URL and HUB_API_KEY. env: {} + # Optional autoscaling for the Hub API deployment. + autoscaling: + enabled: false + additionalLabels: {} + annotations: {} + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: {} + + # Optional PDB for the Hub API deployment. Disabled by default because a single + # Hub replica with minAvailable: 1 blocks voluntary node drains. + pdb: + enabled: false + additionalLabels: {} + annotations: {} + minAvailable: 1 + # maxUnavailable: 1 + # unhealthyPodEvictionPolicy: AlwaysAllow + + worker: + # Hub async jobs (webhook dispatch, embeddings) run in hub-worker. Keep this + # enabled unless another worker deployment processes the same River queues. + enabled: true + replicas: 1 + + # Optional env vars (non-secret) added only to hub-worker. + env: {} + + resources: + limits: + memory: 512Mi + requests: + memory: 256Mi + cpu: "100m" + + autoscaling: + enabled: false + additionalLabels: {} + annotations: {} + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 120 + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + + # Disabled by default because the default worker replica count is 1. + pdb: + enabled: false + additionalLabels: {} + annotations: {} + minAvailable: 1 + # maxUnavailable: 1 + # unhealthyPodEvictionPolicy: AlwaysAllow + + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + + embeddings: + # Optional self-hosted OpenAI-compatible embeddings runtime for Hub. + enabled: false + runtime: tei + model: google/embeddinggemma-300m + revision: "" + # Defaults to `model` when empty. Used by TEI OpenAI-compatible responses + # and as Hub's EMBEDDING_MODEL. + servedModelName: "" + # Defaults to http://-hub-embeddings:/v1 when empty. + baseUrl: "" + maxConcurrent: "5" + normalize: "false" + replicas: 1 + + image: + repository: "ghcr.io/huggingface/text-embeddings-inference" + tag: "cpu-1.9" + pullPolicy: IfNotPresent + + command: [] + # When empty, the chart renders TEI args from model, servedModelName, port, + # revision, and persistence.mountPath. Set this to fully override args. + args: [] + extraArgs: [] + env: {} + + port: 8080 + prometheusPort: 9000 + + service: + type: ClusterIP + port: 8080 + annotations: {} + additionalLabels: {} + + auth: + # TEI can enforce bearer-token auth with API_KEY. Hub always receives the + # same key as EMBEDDING_PROVIDER_API_KEY because the OpenAI-compatible Hub + # provider requires an API key. + enabled: true + existingSecret: "" + secretKey: EMBEDDING_PROVIDER_API_KEY + apiKey: "" + + huggingFace: + # Required for gated models such as google/embeddinggemma-300m unless the + # model is pre-cached or a different ungated model is configured. + existingSecret: "" + tokenKey: HF_TOKEN + token: "" + + persistence: + enabled: true + existingClaim: "" + storageClass: "" + accessModes: + - ReadWriteOnce + size: 10Gi + mountPath: /data + + resources: + requests: + cpu: "4" + memory: 8Gi + limits: + memory: 8Gi + + autoscaling: + enabled: false + additionalLabels: {} + annotations: {} + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 600 + policies: + - type: Pods + value: 1 + periodSeconds: 300 + scaleUp: + stabilizationWindowSeconds: 120 + policies: + - type: Pods + value: 1 + periodSeconds: 120 + + # Disabled by default because the default embeddings replica count is 1. + pdb: + enabled: false + additionalLabels: {} + annotations: {} + minAvailable: 1 + # maxUnavailable: 1 + # unhealthyPodEvictionPolicy: AlwaysAllow + + probes: + startupProbe: + failureThreshold: 60 + periodSeconds: 10 + tcpSocket: + port: http + readinessProbe: + failureThreshold: 6 + periodSeconds: 10 + timeoutSeconds: 5 + tcpSocket: + port: http + livenessProbe: + failureThreshold: 6 + periodSeconds: 20 + timeoutSeconds: 5 + tcpSocket: + port: http + + podAnnotations: {} + podLabels: {} + podSecurityContext: {} + # Keep empty by default because upstream model-serving images may define + # their own user and need write access to the model cache path. + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + # Helm does not deploy Cube. XM Suite v5 analytics requires operators to provide an external Cube instance, # set deployment.env.CUBEJS_API_URL, and supply CUBEJS_API_SECRET via an existing secret.