diff --git a/infrastructure/helm/templates/postgres-statefulset.yaml b/infrastructure/helm/templates/postgres-statefulset.yaml new file mode 100644 index 0000000..fd8724c --- /dev/null +++ b/infrastructure/helm/templates/postgres-statefulset.yaml @@ -0,0 +1,142 @@ +{{- if .Values.postgres.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "tod.fullname" . }}-postgres-init + labels: + {{- include "tod.componentLabels" (dict "context" . "component" "postgres") | nindent 4 }} +data: + init.sql: | + -- Enable TimescaleDB (required for hypertables) + CREATE EXTENSION IF NOT EXISTS timescaledb; + + -- app_user role (RLS enforced, DML only) + DO $$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .Values.postgres.auth.appUsername }}') THEN + CREATE ROLE {{ .Values.postgres.auth.appUsername }} WITH LOGIN PASSWORD '{{ .Values.secrets.dbAppPassword }}'; + END IF; + END $$; + + GRANT CONNECT ON DATABASE {{ .Values.postgres.auth.database }} TO {{ .Values.postgres.auth.appUsername }}; + GRANT USAGE ON SCHEMA public TO {{ .Values.postgres.auth.appUsername }}; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO {{ .Values.postgres.auth.appUsername }}; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO {{ .Values.postgres.auth.appUsername }}; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {{ .Values.postgres.auth.appUsername }}; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT USAGE, SELECT ON SEQUENCES TO {{ .Values.postgres.auth.appUsername }}; + + -- poller_user role (bypasses RLS for cross-tenant device polling) + DO $$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'poller_user') THEN + CREATE ROLE poller_user WITH LOGIN PASSWORD '{{ .Values.secrets.dbPollerPassword }}' NOSUPERUSER NOCREATEDB NOCREATEROLE BYPASSRLS; + END IF; + END $$; + + GRANT CONNECT ON DATABASE {{ .Values.postgres.auth.database }} TO poller_user; + GRANT USAGE ON SCHEMA public TO poller_user; +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "tod.fullname" . }}-postgres + labels: + {{- include "tod.componentLabels" (dict "context" . "component" "postgres") | nindent 4 }} +spec: + serviceName: {{ include "tod.fullname" . }}-postgres + replicas: 1 + selector: + matchLabels: + {{- include "tod.componentSelectorLabels" (dict "context" . "component" "postgres") | nindent 6 }} + template: + metadata: + labels: + {{- include "tod.componentSelectorLabels" (dict "context" . "component" "postgres") | nindent 8 }} + spec: + containers: + - name: postgres + image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" + imagePullPolicy: {{ .Values.postgres.image.pullPolicy }} + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_DB + value: {{ .Values.postgres.auth.database | quote }} + - name: POSTGRES_USER + value: {{ .Values.postgres.auth.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "tod.fullname" . }} + key: DB_PASSWORD + - name: APP_USER + value: {{ .Values.postgres.auth.appUsername | quote }} + - name: APP_USER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "tod.fullname" . }} + key: DB_APP_PASSWORD + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + - name: init-scripts + mountPath: /docker-entrypoint-initdb.d + readOnly: true + resources: + {{- toYaml .Values.postgres.resources | nindent 12 }} + livenessProbe: + exec: + command: + - pg_isready + - -U + - postgres + - -d + - tod + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + readinessProbe: + exec: + command: + - pg_isready + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 3 + volumes: + - name: init-scripts + configMap: + name: {{ include "tod.fullname" . }}-postgres-init + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: + - ReadWriteOnce + {{- if .Values.postgres.storageClass }} + storageClassName: {{ .Values.postgres.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgres.storage }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "tod.fullname" . }}-postgres + labels: + {{- include "tod.componentLabels" (dict "context" . "component" "postgres") | nindent 4 }} +spec: + clusterIP: None + ports: + - name: postgres + port: {{ .Values.postgres.service.port }} + targetPort: postgres + protocol: TCP + selector: + {{- include "tod.componentSelectorLabels" (dict "context" . "component" "postgres") | nindent 4 }} +{{- end }}