diff --git a/api/Dockerfile b/api/Dockerfile index 1a8d48dc..2ef3ddc8 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -141,7 +141,22 @@ ENV PYTHONUNBUFFERED=1 # Copy application code (chown at copy-time avoids a duplicate /app layer # from a later `RUN chown -R`, which would double the on-disk size of /app). COPY --chown=dograh:dograh ./api ./api -COPY --chown=dograh:dograh ./scripts/start_services_docker.sh ./scripts/start_services_docker.sh + +# Entrypoint scripts. +# start_services_docker.sh — single-container (docker-compose) entrypoint +# that runs every service in one process tree. +# run_*.sh — per-service entrypoints used by the Helm chart, +# which runs each workload (web, arq-worker, ari-manager, +# campaign-orchestrator, migrate) as its own pod. Keep this list in sync +# with the command:[] entries in deploy/helm/dograh/templates/*.yaml. +COPY --chown=dograh:dograh \ + ./scripts/start_services_docker.sh \ + ./scripts/run_migrate.sh \ + ./scripts/run_web.sh \ + ./scripts/run_arq_worker.sh \ + ./scripts/run_ari_manager.sh \ + ./scripts/run_campaign_orchestrator.sh \ + ./scripts/ # ts_validator Node deps (built in ts-deps stage with full node:22-slim image). # The validator runs as a short-lived subprocess from api/mcp_server/ts_bridge.py. diff --git a/deploy/helm/dograh/Chart.lock b/deploy/helm/dograh/Chart.lock deleted file mode 100644 index 1097715b..00000000 --- a/deploy/helm/dograh/Chart.lock +++ /dev/null @@ -1,12 +0,0 @@ -dependencies: -- name: postgresql - repository: oci://registry-1.docker.io/bitnamicharts - version: 16.7.27 -- name: redis - repository: oci://registry-1.docker.io/bitnamicharts - version: 20.13.4 -- name: minio - repository: oci://registry-1.docker.io/bitnamicharts - version: 16.0.10 -digest: sha256:8a2d769b21e882239fc847a9638758131f388dbd12feabd387671e9a336a2615 -generated: "2026-05-25T19:49:15.193741+05:30" diff --git a/deploy/helm/dograh/Chart.yaml b/deploy/helm/dograh/Chart.yaml index 91220b34..607d2746 100644 --- a/deploy/helm/dograh/Chart.yaml +++ b/deploy/helm/dograh/Chart.yaml @@ -27,21 +27,11 @@ sources: maintainers: - name: Dograh -# Bundled stateful dependencies. Each is gated by its mode toggle in -# values.yaml so external-managed deployments do not pull these charts. -dependencies: - - name: postgresql - version: "16.x.x" - repository: oci://registry-1.docker.io/bitnamicharts - condition: postgresql.enabled - # Aliased to redisInternal so the dograh-level `redis.mode` toggle does not - # collide with the subchart's values namespace. - - name: redis - alias: redisInternal - version: "20.x.x" - repository: oci://registry-1.docker.io/bitnamicharts - condition: redisInternal.enabled - - name: minio - version: "16.x.x" - repository: oci://registry-1.docker.io/bitnamicharts - condition: minio.enabled +# The bundled stateful deps (PostgreSQL, Redis, MinIO) for the internal/all-in-one +# modes are plain in-chart manifests (templates/internal-*.yaml) built on official +# upstream images — NOT subcharts. This avoids depending on third-party chart +# packaging/registries that can change or disappear (as the Bitnami catalog did in +# Aug 2025). Production deployments should use the external/managed modes +# (database.mode=external, redis.mode=external, storage.mode=s3|externalMinio). +# +# dependencies: [] diff --git a/deploy/helm/dograh/README.md b/deploy/helm/dograh/README.md index 760bf8ed..31308333 100644 --- a/deploy/helm/dograh/README.md +++ b/deploy/helm/dograh/README.md @@ -15,10 +15,9 @@ exercised against a live cluster. ```bash cd deploy/helm/dograh -# Pull in Bitnami subcharts (network required) -helm dependency update - -# Install with defaults (all internal deps, Gateway API exposure) +# Install with defaults (all internal deps, Gateway API exposure). +# The bundled Postgres/Redis/MinIO are in-chart manifests on official upstream +# images — no `helm dependency` / subchart pull step needed. helm install dograh . \ --set secrets.ossJwtSecret="$(openssl rand -hex 32)" \ --set secrets.turnSecret="$(openssl rand -hex 32)" \ diff --git a/deploy/helm/dograh/examples/values-aws.yaml b/deploy/helm/dograh/examples/values-aws.yaml index 69eb3c72..39175b36 100644 --- a/deploy/helm/dograh/examples/values-aws.yaml +++ b/deploy/helm/dograh/examples/values-aws.yaml @@ -62,10 +62,5 @@ autoscaling: minReplicas: 3 maxReplicas: 12 -# Disable bundled deps. -postgresql: - enabled: false -redisInternal: - enabled: false -minio: - enabled: false +# No bundled deps: the in-chart Postgres/Redis/MinIO manifests are gated on the +# internal modes, so the external/S3 modes above already keep them from rendering. diff --git a/deploy/helm/dograh/examples/values-managed.yaml b/deploy/helm/dograh/examples/values-managed.yaml index 4f9084ed..cdb55bc1 100644 --- a/deploy/helm/dograh/examples/values-managed.yaml +++ b/deploy/helm/dograh/examples/values-managed.yaml @@ -62,10 +62,7 @@ autoscaling: maxReplicas: 12 targetCPUUtilizationPercentage: 70 -# Disable bundled deps when using external services. -postgresql: - enabled: false -redisInternal: - enabled: false -minio: - enabled: false +# No bundled deps here: the in-chart Postgres/Redis/MinIO manifests are gated on +# the internal modes (database.mode=internal, redis.mode=internal, +# storage.mode=internalMinio), so the external/S3 modes set above already keep +# them from rendering. diff --git a/deploy/helm/dograh/examples/values-single-node.yaml b/deploy/helm/dograh/examples/values-single-node.yaml index 8f0c3466..b0f293f3 100644 --- a/deploy/helm/dograh/examples/values-single-node.yaml +++ b/deploy/helm/dograh/examples/values-single-node.yaml @@ -49,13 +49,11 @@ autoscaling: enabled: false postgresql: - primary: - persistence: - size: 2Gi -redisInternal: - master: - persistence: - size: 1Gi + persistence: + size: 2Gi +redisinternal: + persistence: + size: 1Gi minio: persistence: size: 5Gi diff --git a/deploy/helm/dograh/templates/NOTES.txt b/deploy/helm/dograh/templates/NOTES.txt index 016c6173..ab74de81 100644 --- a/deploy/helm/dograh/templates/NOTES.txt +++ b/deploy/helm/dograh/templates/NOTES.txt @@ -52,7 +52,7 @@ coturn is disabled. Set coturn.enabled=true to deploy media relay. === Migrations === {{- if .Values.migrate.enabled }} -Alembic migrations run as a pre-install / pre-upgrade hook. Inspect with: +Alembic migrations run as a post-install / pre-upgrade hook. Inspect with: kubectl logs job/{{ include "dograh.migrate.fullname" . }} -n {{ .Release.Namespace }} {{- end }} diff --git a/deploy/helm/dograh/templates/_helpers.tpl b/deploy/helm/dograh/templates/_helpers.tpl index 2d497f65..d8469848 100644 --- a/deploy/helm/dograh/templates/_helpers.tpl +++ b/deploy/helm/dograh/templates/_helpers.tpl @@ -98,12 +98,60 @@ In-cluster service references for internal deps. {{- define "dograh.redisHost" -}}{{ .Release.Name }}-redisinternal-master{{- end }} {{- define "dograh.minioHost" -}}{{ .Release.Name }}-minio{{- end }} +{{/* +Resolved passwords for the bundled internal deps. +Precedence: explicit value in values.yaml wins; else reuse the value already +stored in the Secret (so `helm upgrade` does NOT rotate the password and desync a +running datastore); else generate a fresh one. `lookup` returns empty during +`helm template` (no cluster), so dry renders get a throwaway random value — fine. +Each is materialized in exactly one place (the dep's Secret); every other +reference is a secretKeyRef, so the generated value is stable within a render. +*/}} +{{- define "dograh.postgresPassword" -}} +{{- if .Values.postgresql.auth.password -}} +{{- .Values.postgresql.auth.password -}} +{{- else -}} +{{- $s := lookup "v1" "Secret" .Release.Namespace (printf "%s-postgresql" .Release.Name) -}} +{{- if and $s $s.data (index $s.data "password") -}} +{{- index $s.data "password" | b64dec -}} +{{- else -}} +{{- randAlphaNum 24 -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{- define "dograh.redisPassword" -}} +{{- if .Values.redisinternal.auth.password -}} +{{- .Values.redisinternal.auth.password -}} +{{- else -}} +{{- $s := lookup "v1" "Secret" .Release.Namespace (printf "%s-redisinternal" .Release.Name) -}} +{{- if and $s $s.data (index $s.data "redis-password") -}} +{{- index $s.data "redis-password" | b64dec -}} +{{- else -}} +{{- randAlphaNum 24 -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{- define "dograh.minioRootPassword" -}} +{{- if .Values.minio.auth.rootPassword -}} +{{- .Values.minio.auth.rootPassword -}} +{{- else -}} +{{- $s := lookup "v1" "Secret" .Release.Namespace (printf "%s-minio" .Release.Name) -}} +{{- if and $s $s.data (index $s.data "root-password") -}} +{{- index $s.data "root-password" | b64dec -}} +{{- else -}} +{{- randAlphaNum 24 -}} +{{- end -}} +{{- end -}} +{{- end }} + {{/* Default DATABASE_URL when database.mode=internal. -Bitnami Postgres exposes the password as -postgresql secret key -`postgres-password` or `password`. The chart pulls it via envFrom on the -generated secret; for clarity we still need the URL string. Auth username -defaults to `dograh` (see values.postgresql.auth.username). +The bundled Postgres (templates/internal-postgres.yaml) stores the app-user +password in the -postgresql Secret under key `password`; dograh.dbEnv +projects it into $(POSTGRES_PASSWORD), which this URL interpolates at runtime. +Auth username/database default to `dograh` (see values.postgresql.auth). */}} {{- define "dograh.databaseUrl" -}} {{- if eq .Values.database.mode "internal" -}} @@ -121,6 +169,53 @@ $(REDIS_URL) {{- end -}} {{- end }} +{{/* +Database / Redis connection env for backend workloads (web, arq, singletons, +migrate). + +ORDER IS LOAD-BEARING. POSTGRES_PASSWORD / REDIS_PASSWORD are declared BEFORE +DATABASE_URL / REDIS_URL because Kubernetes only expands a $(VAR) reference to +an env var defined *earlier* in the same container's env list — a forward +reference is left as the literal string "$(VAR)". DATABASE_URL / REDIS_URL +embed $(POSTGRES_PASSWORD) / $(REDIS_PASSWORD) (see dograh.databaseUrl / +dograh.redisUrl), so the password vars must come first or the composed URLs +ship with a literal "$(POSTGRES_PASSWORD)" as the password. +*/}} +{{- define "dograh.dbEnv" -}} +{{- if eq .Values.database.mode "internal" }} +- name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgresql + key: password +{{- end }} +{{- if eq .Values.redis.mode "internal" }} +- name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-redisinternal + key: redis-password +{{- end }} +- name: DATABASE_URL + value: {{ include "dograh.databaseUrl" . | quote }} +- name: REDIS_URL + value: {{ include "dograh.redisUrl" . | quote }} +{{- if eq .Values.storage.mode "internalMinio" }} +{{- /* Internal MinIO creds come from the -minio secret, the same + source the MinIO server uses (no ordering constraint — no composition). */}} +- name: MINIO_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-minio + key: root-user +- name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-minio + key: root-password +{{- end }} +{{- end }} + {{/* Common env block for backend workloads (web, arq, singletons, migrate). References the ConfigMap + Secret via envFrom. DATABASE_URL and REDIS_URL @@ -131,16 +226,6 @@ are added inline because they may need composition from subchart secrets. name: {{ include "dograh.configMapName" . }} - secretRef: name: {{ include "dograh.secretName" . }} -{{- if eq .Values.database.mode "internal" }} -- secretRef: - name: {{ .Release.Name }}-postgresql - optional: true -{{- end }} -{{- if eq .Values.redis.mode "internal" }} -- secretRef: - name: {{ .Release.Name }}-redisinternal - optional: true -{{- end }} {{- end }} {{/* diff --git a/deploy/helm/dograh/templates/ari-manager-deployment.yaml b/deploy/helm/dograh/templates/ari-manager-deployment.yaml index 80b668ee..9bc83252 100644 --- a/deploy/helm/dograh/templates/ari-manager-deployment.yaml +++ b/deploy/helm/dograh/templates/ari-manager-deployment.yaml @@ -43,24 +43,7 @@ spec: envFrom: {{- include "dograh.backendEnvFrom" . | nindent 12 }} env: - - name: DATABASE_URL - value: {{ include "dograh.databaseUrl" . | quote }} - - name: REDIS_URL - value: {{ include "dograh.redisUrl" . | quote }} - {{- if eq .Values.database.mode "internal" }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-postgresql - key: password - {{- end }} - {{- if eq .Values.redis.mode "internal" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-redisinternal - key: redis-password - {{- end }} + {{- include "dograh.dbEnv" . | nindent 12 }} # exec probe — no HTTP endpoint exists on ari-manager. livenessProbe: {{- toYaml .Values.ariManager.livenessProbe | nindent 12 }} diff --git a/deploy/helm/dograh/templates/arq-worker-deployment.yaml b/deploy/helm/dograh/templates/arq-worker-deployment.yaml index 95fb6bbb..4af2fc5a 100644 --- a/deploy/helm/dograh/templates/arq-worker-deployment.yaml +++ b/deploy/helm/dograh/templates/arq-worker-deployment.yaml @@ -40,24 +40,7 @@ spec: envFrom: {{- include "dograh.backendEnvFrom" . | nindent 12 }} env: - - name: DATABASE_URL - value: {{ include "dograh.databaseUrl" . | quote }} - - name: REDIS_URL - value: {{ include "dograh.redisUrl" . | quote }} - {{- if eq .Values.database.mode "internal" }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-postgresql - key: password - {{- end }} - {{- if eq .Values.redis.mode "internal" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-redisinternal - key: redis-password - {{- end }} + {{- include "dograh.dbEnv" . | nindent 12 }} livenessProbe: {{- toYaml .Values.workers.livenessProbe | nindent 12 }} resources: diff --git a/deploy/helm/dograh/templates/campaign-orchestrator-deployment.yaml b/deploy/helm/dograh/templates/campaign-orchestrator-deployment.yaml index 22a2a567..8e1ef2c9 100644 --- a/deploy/helm/dograh/templates/campaign-orchestrator-deployment.yaml +++ b/deploy/helm/dograh/templates/campaign-orchestrator-deployment.yaml @@ -43,24 +43,7 @@ spec: envFrom: {{- include "dograh.backendEnvFrom" . | nindent 12 }} env: - - name: DATABASE_URL - value: {{ include "dograh.databaseUrl" . | quote }} - - name: REDIS_URL - value: {{ include "dograh.redisUrl" . | quote }} - {{- if eq .Values.database.mode "internal" }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-postgresql - key: password - {{- end }} - {{- if eq .Values.redis.mode "internal" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-redisinternal - key: redis-password - {{- end }} + {{- include "dograh.dbEnv" . | nindent 12 }} # exec probe — no HTTP endpoint exists on campaign-orchestrator. livenessProbe: {{- toYaml .Values.campaignOrchestrator.livenessProbe | nindent 12 }} diff --git a/deploy/helm/dograh/templates/internal-minio.yaml b/deploy/helm/dograh/templates/internal-minio.yaml new file mode 100644 index 00000000..f3760462 --- /dev/null +++ b/deploy/helm/dograh/templates/internal-minio.yaml @@ -0,0 +1,137 @@ +{{- if eq .Values.storage.mode "internalMinio" }} +{{- /* +Bundled MinIO for storage.mode=internalMinio, official upstream image. Single +replica (Recreate strategy — the RWO volume can't be shared). The app creates +its bucket on first use (bucket_exists → make_bucket), so no provisioning Job. + +The root credentials live in -minio and are consumed BOTH by the MinIO +server (MINIO_ROOT_USER / MINIO_ROOT_PASSWORD) and by the app (MINIO_ACCESS_KEY / +MINIO_SECRET_KEY, wired in dograh.dbEnv) — one source of truth, so they can't +drift. For production use storage.mode=s3 or externalMinio. +*/}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-minio + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: minio +type: Opaque +stringData: + root-user: {{ .Values.minio.auth.rootUser | default "minioadmin" | quote }} + root-password: {{ include "dograh.minioRootPassword" . | quote }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-minio + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: minio +spec: + type: ClusterIP + ports: + - name: api + port: 9000 + targetPort: api + - name: console + port: 9001 + targetPort: console + selector: + {{- include "dograh.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: minio +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Release.Name }}-minio + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: minio +spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.minio.persistence.storageClass }} + storageClassName: {{ .Values.minio.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.minio.persistence.size | quote }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-minio + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: minio +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "dograh.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: minio + template: + metadata: + labels: + {{- include "dograh.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: minio + spec: + securityContext: + fsGroup: 1000 + containers: + - name: minio + image: {{ printf "%s/%s:%s" (.Values.minio.image.registry | default "docker.io") .Values.minio.image.repository (.Values.minio.image.tag | toString) | quote }} + imagePullPolicy: {{ .Values.minio.image.pullPolicy | default "IfNotPresent" }} + args: + - server + - /data + - --console-address + - ":9001" + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-minio + key: root-user + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-minio + key: root-password + ports: + - name: api + containerPort: 9000 + - name: console + containerPort: 9001 + readinessProbe: + httpGet: + path: /minio/health/ready + port: api + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /minio/health/live + port: api + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + resources: + {{- toYaml .Values.minio.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ .Release.Name }}-minio +{{- end }} diff --git a/deploy/helm/dograh/templates/internal-postgres.yaml b/deploy/helm/dograh/templates/internal-postgres.yaml new file mode 100644 index 00000000..46a83ecf --- /dev/null +++ b/deploy/helm/dograh/templates/internal-postgres.yaml @@ -0,0 +1,118 @@ +{{- if eq .Values.database.mode "internal" }} +{{- /* +Bundled PostgreSQL for database.mode=internal. + +Image is the official pgvector build — upstream postgres plus the `vector` +extension the app's migrations require (CREATE EXTENSION vector). POSTGRES_USER +is the initdb superuser, so the migration can create the (untrusted) extension. + +This is the convenience/all-in-one path. For production, set database.mode=external +and point secrets.databaseUrl at a managed Postgres. +*/}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-postgresql + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +type: Opaque +stringData: + password: {{ include "dograh.postgresPassword" . | quote }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-postgresql + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + type: ClusterIP + ports: + - name: postgresql + port: 5432 + targetPort: postgresql + selector: + {{- include "dograh.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Release.Name }}-postgresql + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + serviceName: {{ .Release.Name }}-postgresql + replicas: 1 + selector: + matchLabels: + {{- include "dograh.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: postgresql + template: + metadata: + labels: + {{- include "dograh.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: postgresql + spec: + securityContext: + # Official image starts as root, chowns PGDATA, then drops to the + # `postgres` uid (999). fsGroup makes the mounted volume group-writable. + fsGroup: 999 + containers: + - name: postgresql + image: {{ printf "%s/%s:%s" (.Values.postgresql.image.registry | default "docker.io") .Values.postgresql.image.repository (.Values.postgresql.image.tag | toString) | quote }} + imagePullPolicy: {{ .Values.postgresql.image.pullPolicy | default "IfNotPresent" }} + ports: + - name: postgresql + containerPort: 5432 + env: + - name: POSTGRES_USER + value: {{ .Values.postgresql.auth.username | quote }} + - name: POSTGRES_DB + value: {{ .Values.postgresql.auth.database | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgresql + key: password + # Init into a subdirectory so a mounted volume's lost+found does not + # collide with initdb's empty-dir check. + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + readinessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgresql.auth.username | quote }}, "-d", {{ .Values.postgresql.auth.database | quote }}] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgresql.auth.username | quote }}, "-d", {{ .Values.postgresql.auth.database | quote }}] + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + resources: + {{- toYaml .Values.postgresql.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.postgresql.persistence.storageClass }} + storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgresql.persistence.size | quote }} +{{- end }} diff --git a/deploy/helm/dograh/templates/internal-redis.yaml b/deploy/helm/dograh/templates/internal-redis.yaml new file mode 100644 index 00000000..c3f4caa5 --- /dev/null +++ b/deploy/helm/dograh/templates/internal-redis.yaml @@ -0,0 +1,115 @@ +{{- if eq .Values.redis.mode "internal" }} +{{- /* +Bundled Redis for redis.mode=internal, official upstream image. Single-node, +password-protected, AOF persistence. For production set redis.mode=external and +point secrets.redisUrl at a managed Redis/Valkey. + +Service name is -redisinternal-master (kept from the previous packaging) +so dograh.redisHost / dograh.redisUrl need no change. +*/}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-redisinternal + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +type: Opaque +stringData: + redis-password: {{ include "dograh.redisPassword" . | quote }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-redisinternal-master + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + type: ClusterIP + ports: + - name: redis + port: 6379 + targetPort: redis + selector: + {{- include "dograh.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Release.Name }}-redisinternal-master + namespace: {{ .Release.Namespace }} + labels: + {{- include "dograh.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + serviceName: {{ .Release.Name }}-redisinternal-master + replicas: 1 + selector: + matchLabels: + {{- include "dograh.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: redis + template: + metadata: + labels: + {{- include "dograh.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: redis + spec: + securityContext: + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + containers: + - name: redis + image: {{ printf "%s/%s:%s" (.Values.redisinternal.image.registry | default "docker.io") .Values.redisinternal.image.repository (.Values.redisinternal.image.tag | toString) | quote }} + imagePullPolicy: {{ .Values.redisinternal.image.pullPolicy | default "IfNotPresent" }} + command: ["redis-server"] + # $(REDIS_PASSWORD) is expanded by Kubernetes from the env var below. + args: + - "--requirepass" + - "$(REDIS_PASSWORD)" + - "--appendonly" + - "yes" + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-redisinternal + key: redis-password + ports: + - name: redis + containerPort: 6379 + readinessProbe: + exec: + command: ["sh", "-c", 'redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG'] + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + exec: + command: ["sh", "-c", 'redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG'] + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + resources: + {{- toYaml .Values.redisinternal.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.redisinternal.persistence.storageClass }} + storageClassName: {{ .Values.redisinternal.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.redisinternal.persistence.size | quote }} +{{- end }} diff --git a/deploy/helm/dograh/templates/migrate-job.yaml b/deploy/helm/dograh/templates/migrate-job.yaml index 20b6c82e..5183a2b8 100644 --- a/deploy/helm/dograh/templates/migrate-job.yaml +++ b/deploy/helm/dograh/templates/migrate-job.yaml @@ -8,7 +8,14 @@ metadata: {{- include "dograh.labels" . | nindent 4 }} app.kubernetes.io/component: migrate annotations: - "helm.sh/hook": pre-install,pre-upgrade + # post-install (not pre-install): when the bundled Postgres subchart is in + # use it is a normal resource, so on a fresh install it does not exist until + # after pre-install hooks finish — a pre-install migration would deadlock + # waiting for a database Helm has not created yet. post-install runs after + # all resources (incl. Postgres) are applied. pre-upgrade is kept so + # migrations land before new app code rolls out on upgrades (where the DB + # already exists). + "helm.sh/hook": post-install,pre-upgrade "helm.sh/hook-weight": "-5" "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: @@ -27,6 +34,26 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- if eq .Values.database.mode "internal" }} + # post-install fires right after resources are applied — the bundled + # Postgres pod may still be starting. Block until its Service routes to a + # ready endpoint (endpoints only publish once the pod passes readiness) + # before alembic attempts to connect. Reuses the app image (already + # pulled for the migrate container) so no extra image is needed. + initContainers: + - name: wait-for-postgres + image: {{ include "dograh.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - bash + - -c + - | + until (exec 3<>/dev/tcp/{{ include "dograh.postgresHost" . }}/5432) 2>/dev/null; do + echo "waiting for postgres at {{ include "dograh.postgresHost" . }}:5432 ..." + sleep 2 + done + echo "postgres is reachable" + {{- end }} containers: - name: migrate image: {{ include "dograh.image" . }} @@ -35,15 +62,7 @@ spec: envFrom: {{- include "dograh.backendEnvFrom" . | nindent 12 }} env: - - name: DATABASE_URL - value: {{ include "dograh.databaseUrl" . | quote }} - {{- if eq .Values.database.mode "internal" }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-postgresql - key: password - {{- end }} + {{- include "dograh.dbEnv" . | nindent 12 }} resources: {{- toYaml .Values.migrate.resources | nindent 12 }} {{- end }} diff --git a/deploy/helm/dograh/templates/secret.yaml b/deploy/helm/dograh/templates/secret.yaml index 5af798c0..01bda8a1 100644 --- a/deploy/helm/dograh/templates/secret.yaml +++ b/deploy/helm/dograh/templates/secret.yaml @@ -16,7 +16,9 @@ stringData: {{- if eq .Values.redis.mode "external" }} REDIS_URL: {{ required "secrets.redisUrl is required when redis.mode=external" .Values.secrets.redisUrl | quote }} {{- end }} - {{- if or (eq .Values.storage.mode "internalMinio") (eq .Values.storage.mode "externalMinio") }} + {{- /* internalMinio creds are sourced from the -minio secret via + dograh.dbEnv; only externalMinio pulls creds from here. */}} + {{- if eq .Values.storage.mode "externalMinio" }} MINIO_ACCESS_KEY: {{ .Values.secrets.minioAccessKey | quote }} MINIO_SECRET_KEY: {{ .Values.secrets.minioSecretKey | quote }} {{- end }} diff --git a/deploy/helm/dograh/templates/ui-deployment.yaml b/deploy/helm/dograh/templates/ui-deployment.yaml index 9285fc32..9282220d 100644 --- a/deploy/helm/dograh/templates/ui-deployment.yaml +++ b/deploy/helm/dograh/templates/ui-deployment.yaml @@ -42,6 +42,13 @@ spec: containerPort: {{ .Values.ui.port }} protocol: TCP env: + # Next.js standalone binds to $HOSTNAME (falls back to 0.0.0.0). + # Kubernetes injects HOSTNAME=, which /etc/hosts maps to the + # pod IP — so without this, Next listens on the pod IP ONLY. In-cluster + # Service traffic still works, but loopback (kubectl port-forward, local + # health checks) is refused. Pin to 0.0.0.0 to listen on all interfaces. + - name: HOSTNAME + value: "0.0.0.0" - name: NODE_ENV value: "oss" - name: BACKEND_URL diff --git a/deploy/helm/dograh/templates/web-deployment.yaml b/deploy/helm/dograh/templates/web-deployment.yaml index 6fdb585a..c4829d56 100644 --- a/deploy/helm/dograh/templates/web-deployment.yaml +++ b/deploy/helm/dograh/templates/web-deployment.yaml @@ -52,24 +52,7 @@ spec: env: - name: WEB_PORT value: {{ .Values.web.port | quote }} - - name: DATABASE_URL - value: {{ include "dograh.databaseUrl" . | quote }} - - name: REDIS_URL - value: {{ include "dograh.redisUrl" . | quote }} - {{- if eq .Values.database.mode "internal" }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-postgresql - key: password - {{- end }} - {{- if eq .Values.redis.mode "internal" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-redisinternal - key: redis-password - {{- end }} + {{- include "dograh.dbEnv" . | nindent 12 }} # Distinct probes: readiness flips fast (drain), liveness is # slower (process aliveness). livenessProbe: diff --git a/deploy/helm/dograh/values.yaml b/deploy/helm/dograh/values.yaml index 5c53d134..2cb09b9c 100644 --- a/deploy/helm/dograh/values.yaml +++ b/deploy/helm/dograh/values.yaml @@ -32,13 +32,13 @@ serviceAccount: # Stateful dependency modes. # # database.mode: -# internal — bundled Bitnami PostgreSQL subchart (postgresql.enabled=true) +# internal — bundled Postgres manifest (pgvector image; see `postgresql` below) # external — operator supplies DATABASE_URL via secrets.databaseUrl # redis.mode: -# internal — bundled Bitnami Redis subchart (redis.enabled=true) +# internal — bundled Redis manifest (see `redisinternal` below) # external — operator supplies REDIS_URL via secrets.redisUrl # storage.mode: -# internalMinio — bundled MinIO subchart (minio.enabled=true) +# internalMinio — bundled MinIO manifest (see `minio` below) # externalMinio — operator supplies a MinIO-compatible endpoint + creds # s3 — sets ENABLE_AWS_S3=true; uses AWS S3 # exposure.mode: @@ -253,7 +253,10 @@ workers: # exec probe — workers have no HTTP endpoint. livenessProbe: exec: - command: ["sh", "-c", "pgrep -f 'arq api.tasks.arq.WorkerSettings' > /dev/null"] + # The entrypoint exec's the worker, so it runs as PID 1; grep its cmdline. + # Avoids pgrep/procps, which isn't in the slim runtime image. Matching a + # single argv token (no spaces) — argv is NUL-separated in /proc. + command: ["sh", "-c", "grep -qa api.tasks.arq.WorkerSettings /proc/1/cmdline"] initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 @@ -285,7 +288,8 @@ ariManager: livenessProbe: exec: - command: ["sh", "-c", "pgrep -f api.services.telephony.ari_manager > /dev/null"] + # PID 1 cmdline check (procps-free; see workers.livenessProbe). + command: ["sh", "-c", "grep -qa api.services.telephony.ari_manager /proc/1/cmdline"] initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 @@ -316,7 +320,8 @@ campaignOrchestrator: livenessProbe: exec: - command: ["sh", "-c", "pgrep -f api.services.campaign.campaign_orchestrator > /dev/null"] + # PID 1 cmdline check (procps-free; see workers.livenessProbe). + command: ["sh", "-c", "grep -qa api.services.campaign.campaign_orchestrator /proc/1/cmdline"] initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 @@ -452,7 +457,8 @@ coturn: # Migration Job # ----------------------------------------------------------------------------- migrate: - # Run alembic upgrade head as a pre-install / pre-upgrade Helm hook. + # Run alembic upgrade head as a Helm hook: post-install (so the bundled + # Postgres exists first) and pre-upgrade (so migrations land before new code). enabled: true # Hard cap on how long a migration may run. A failed/exceeded migration @@ -484,44 +490,65 @@ autoscaling: targetMemoryUtilizationPercentage: 80 # ----------------------------------------------------------------------------- -# Bitnami subcharts. Each is gated by its own `enabled` flag set by mode -# resolution in templates/_helpers.tpl. Override the subchart's own values -# under these keys. +# Bundled stateful deps for the internal/all-in-one modes. These are plain +# in-chart manifests (templates/internal-*.yaml) on official upstream images — +# not subcharts. Each renders only in its internal mode: +# postgresql -> database.mode == internal +# redisinternal-> redis.mode == internal +# minio -> storage.mode == internalMinio +# For production, use the external/managed modes instead (see database/redis/ +# storage above). # ----------------------------------------------------------------------------- postgresql: - # enabled is set automatically based on database.mode in _helpers.tpl; - # this key is read by the subchart's `condition`. - enabled: true + # Official pgvector image = upstream Postgres + the `vector` extension the app + # migrations require. Keep the tag on a supported Postgres major. + image: + registry: docker.io + repository: pgvector/pgvector + tag: pg17 + pullPolicy: IfNotPresent auth: - username: dograh - password: "" # auto-generated if empty + username: dograh # also the initdb superuser (needed for CREATE EXTENSION) + password: "" # auto-generated and persisted across upgrades if empty database: dograh - primary: - persistence: - enabled: true - size: 8Gi + persistence: + size: 8Gi + storageClass: "" # "" = cluster default StorageClass + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: "1", memory: 1Gi } -redisInternal: - # Bitnami Redis subchart values, aliased to redisInternal to avoid - # colliding with `redis.mode` above. `redisInternal.enabled` is the gating - # flag for whether the subchart deploys. - enabled: true +redisinternal: + # Official upstream Redis. Single node, password-protected, AOF persistence. + # (Key kept as `redisinternal` so it doesn't collide with `redis.mode` above.) + image: + registry: docker.io + repository: redis + tag: 7.4-alpine + pullPolicy: IfNotPresent auth: - enabled: true - password: "" # auto-generated if empty - master: - persistence: - enabled: true - size: 8Gi - replica: - replicaCount: 0 # standalone primary by default + password: "" # auto-generated and persisted across upgrades if empty + persistence: + size: 8Gi + storageClass: "" + resources: + requests: { cpu: 50m, memory: 64Mi } + limits: { cpu: 500m, memory: 256Mi } minio: - enabled: true + # Official upstream MinIO. Root creds are shared with the app (single source of + # truth) so they can't drift; the app creates its bucket on first use. + image: + registry: docker.io + repository: minio/minio + tag: RELEASE.2025-04-22T22-12-26Z + pullPolicy: IfNotPresent auth: rootUser: minioadmin - rootPassword: "" # auto-generated if empty - defaultBuckets: "voice-audio" + rootPassword: "" # auto-generated and persisted across upgrades if empty persistence: - enabled: true size: 20Gi + storageClass: "" + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: "1", memory: 1Gi }