Compare commits

...

1 Commits

Author SHA1 Message Date
Bhagya Amarasinghe 46dfa15756 fix: order Helm Hub migrations after Prisma 2026-05-21 18:13:19 +05:30
6 changed files with 202 additions and 1 deletions
+6
View File
@@ -65,6 +65,8 @@ Cube is part of the baseline Formbricks v5 stack and is deployed by this chart b
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`.
When the Formbricks migration job is enabled, Hub waits for the `formbricks-migration` Job to complete before its own goose/river init migrations run. This keeps fresh shared-database installs from creating Hub tables before Prisma has initialized the Formbricks schema.
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
@@ -220,6 +222,10 @@ Autoscaling is opt-in for Hub API, Hub worker, and the embeddings runtime. If yo
| hub.migration.activeDeadlineSeconds | int | `900` | |
| hub.migration.backoffLimit | int | `3` | |
| hub.migration.ttlSecondsAfterFinished | int | `300` | |
| hub.migration.waitForFormbricksMigration.enabled | bool | `true` | |
| hub.migration.waitForFormbricksMigration.intervalSeconds | int | `5` | |
| hub.migration.waitForFormbricksMigration.maxAttempts | int | `180` | |
| hub.migration.waitForFormbricksMigration.missingJobMaxAttempts | int | `12` | |
| hub.pdb.enabled | bool | `false` | |
| hub.replicas | int | `1` | |
| hub.resources.limits.memory | string | `"512Mi"` | |
+8
View File
@@ -125,10 +125,18 @@ If `namespaceOverride` is provided, it will be used; otherwise, it defaults to `
{{- printf "%s-app-secrets" (include "formbricks.name" .) -}}
{{- end }}
{{- define "formbricks.migrationJobName" -}}
{{- printf "%s-migration" (include "formbricks.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "formbricks.hubSecretName" -}}
{{- default (include "formbricks.appSecretName" .) .Values.hub.existingSecret -}}
{{- end }}
{{- define "formbricks.hubMigrationWaitServiceAccountName" -}}
{{- printf "%s-migration-wait" (include "formbricks.hubname" .) | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{/*
Hub image reference. Pin by digest in production (hub.image.digest = "sha256:..."); falls back to
hub.image.tag for local/dev. All Hub workloads (deployment, init container, migration job, future
@@ -32,7 +32,134 @@ spec:
imagePullSecrets:
{{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }}
{{- end }}
{{- if and .Values.migration.enabled .Values.hub.migration.waitForFormbricksMigration.enabled }}
serviceAccountName: {{ include "formbricks.hubMigrationWaitServiceAccountName" . }}
automountServiceAccountToken: true
{{- end }}
initContainers:
{{- if and .Values.migration.enabled .Values.hub.migration.waitForFormbricksMigration.enabled }}
- name: wait-for-formbricks-migration
image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion | default "latest" }}
imagePullPolicy: {{ .Values.deployment.image.pullPolicy }}
command:
- node
- -e
- |
const fs = require("fs");
const https = require("https");
const maxAttempts = Number.parseInt(process.env.FORMBRICKS_MIGRATION_WAIT_MAX_ATTEMPTS || "180", 10);
const missingJobMaxAttempts = Number.parseInt(
process.env.FORMBRICKS_MIGRATION_WAIT_MISSING_JOB_MAX_ATTEMPTS || "12",
10
);
const intervalSeconds = Number.parseInt(process.env.FORMBRICKS_MIGRATION_WAIT_INTERVAL_SECONDS || "5", 10);
const jobName = process.env.FORMBRICKS_MIGRATION_JOB_NAME;
const namespace = fs
.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "utf8")
.trim();
const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
const ca = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const jobPath = `/apis/batch/v1/namespaces/${namespace}/jobs/${jobName}`;
const fetchJob = () =>
new Promise((resolve, reject) => {
const request = https.request(
{
host,
port,
path: jobPath,
method: "GET",
ca,
headers: {
Authorization: `Bearer ${token}`,
},
},
(response) => {
let body = "";
response.setEncoding("utf8");
response.on("data", (chunk) => {
body += chunk;
});
response.on("end", () => {
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(JSON.parse(body));
return;
}
const error = new Error(`Kubernetes API returned ${response.statusCode}: ${body}`);
error.statusCode = response.statusCode;
reject(error);
});
}
);
request.on("error", reject);
request.end();
});
(async () => {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
const job = await fetchJob();
const conditions = job.status?.conditions || [];
const isComplete = conditions.some(
(condition) => condition.type === "Complete" && condition.status === "True"
);
const isFailed = conditions.some(
(condition) => condition.type === "Failed" && condition.status === "True"
);
if (isComplete) {
console.log(`${jobName} completed; starting Hub migrations.`);
return;
}
if (isFailed) {
console.error(`${jobName} failed; refusing to start Hub migrations.`);
process.exitCode = 1;
return;
}
console.log(`Waiting for ${jobName} to complete (${attempt}/${maxAttempts})...`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (error && error.statusCode === 404 && attempt >= missingJobMaxAttempts) {
console.log(`${jobName} was not found after ${attempt} attempts; assuming it was already cleaned up.`);
return;
}
console.log(`Waiting for ${jobName} to be available (${attempt}/${maxAttempts}): ${message}`);
}
await sleep(intervalSeconds * 1000);
}
console.error(`Timed out waiting for ${jobName} after ${maxAttempts} attempts.`);
process.exitCode = 1;
})()
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
env:
- name: FORMBRICKS_MIGRATION_JOB_NAME
value: {{ include "formbricks.migrationJobName" . | quote }}
- name: FORMBRICKS_MIGRATION_WAIT_MAX_ATTEMPTS
value: {{ .Values.hub.migration.waitForFormbricksMigration.maxAttempts | quote }}
- name: FORMBRICKS_MIGRATION_WAIT_MISSING_JOB_MAX_ATTEMPTS
value: {{ .Values.hub.migration.waitForFormbricksMigration.missingJobMaxAttempts | quote }}
- name: FORMBRICKS_MIGRATION_WAIT_INTERVAL_SECONDS
value: {{ .Values.hub.migration.waitForFormbricksMigration.intervalSeconds | quote }}
{{- if .Values.migration.resources }}
resources:
{{- toYaml .Values.migration.resources | nindent 12 }}
{{- end }}
{{- end }}
- name: hub-migrate
image: {{ include "formbricks.hubImage" . }}
imagePullPolicy: {{ .Values.hub.image.pullPolicy }}
@@ -0,0 +1,55 @@
{{- if and .Values.migration.enabled .Values.hub.migration.waitForFormbricksMigration.enabled }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "formbricks.hubMigrationWaitServiceAccountName" . }}
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-migration-wait
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "formbricks.hubMigrationWaitServiceAccountName" . }}
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-migration-wait
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
rules:
- apiGroups:
- batch
resources:
- jobs
resourceNames:
- {{ include "formbricks.migrationJobName" . }}
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "formbricks.hubMigrationWaitServiceAccountName" . }}
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-migration-wait
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "formbricks.hubMigrationWaitServiceAccountName" . }}
subjects:
- kind: ServiceAccount
name: {{ include "formbricks.hubMigrationWaitServiceAccountName" . }}
namespace: {{ include "formbricks.namespace" . }}
{{- end }}
@@ -3,7 +3,7 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "formbricks.name" . }}-migration
name: {{ include "formbricks.migrationJobName" . }}
labels:
{{- include "formbricks.labels" . | nindent 4 }}
annotations:
+5
View File
@@ -909,6 +909,11 @@ hub:
ttlSecondsAfterFinished: 300
backoffLimit: 3
activeDeadlineSeconds: 900
waitForFormbricksMigration:
enabled: true
maxAttempts: 180
missingJobMaxAttempts: 12
intervalSeconds: 5
resources:
limits: