{{- if .Values.statefulSet.enabled }} {{- /* ======================================== VALIDATION BLOCK: Multi-replica requirements ======================================== Enforces constraints for scaling beyond 1 replica: 1. Database: Must use PostgreSQL/MySQL (not SQLite) 2. Persistence: Must be enabled 3. Storage sharing: Must use either existingClaim or mode=shared with create.enabled 4. Access mode: For mode=shared + create, must specify ReadWriteMany */}} {{- $replicas := int .Values.replicaCount }} {{- $dburi := "" }} {{- if and .Values.config (hasKey .Values.config "dbUri") }} {{- $dburi = (index .Values.config "dbUri") }} {{- else if and .Values.config (hasKey .Values.config "db-uri") }} {{- $dburi = (index .Values.config "db-uri") }} {{- end }} {{- $scheme := "" }} {{- if ne $dburi "" }} {{- $parts := splitList "://" $dburi }} {{- if gt (len $parts) 0 }} {{- $scheme = lower (index $parts 0) }} {{- end }} {{- end }} {{- $multiAllowed := or (eq $scheme "postgres") (eq $scheme "postgresql") (eq $scheme "mysql") (eq $scheme "mariadb") }} {{- $p := .Values.persistence }} {{- $mode := default "perReplica" $p.mode }} {{- $hasExisting := ne (default "" $p.existingClaim) "" }} {{- $isShared := eq $mode "shared" }} {{- /* Fail fast: Database validation */}} {{- if and (gt $replicas 1) (not $multiAllowed) }} {{- fail (printf "replicaCount=%d requires PostgreSQL/MySQL config.db-uri; scheme '%s' unsupported" $replicas $scheme) }} {{- end }} {{- /* Fail fast: Persistence must be enabled */}} {{- if and (gt $replicas 1) (not $p.enabled) }} {{- fail (printf "replicaCount=%d requires persistence.enabled=true" $replicas) }} {{- end }} {{- /* Fail fast: Prevent per-replica PVC divergence */}} {{- if and (gt $replicas 1) (not (or $hasExisting $isShared)) }} {{- fail (printf "replicaCount=%d requires either persistence.existingClaim (shared RWX PVC) OR persistence.mode=shared to create one; perReplica PVCs would diverge" $replicas) }} {{- end }} {{- /* Fail fast: Shared mode requires PVC source */}} {{- if and (gt $replicas 1) $isShared (not $hasExisting) (hasKey $p "create") (not (get $p.create "enabled")) }} {{- fail (printf "persistence.mode=shared but neither existingClaim nor create.enabled=true provided") }} {{- end }} {{- /* Fail fast: Auto-created shared PVC must be RWX */}} {{- if and (gt $replicas 1) $isShared (not $hasExisting) $p.create.enabled }} {{- $am := list }} {{- if hasKey $p.create "accessModes" }} {{- $am = $p.create.accessModes }} {{- end }} {{- $rwxOk := false }} {{- range $am }} {{- if or (eq . "ReadWriteMany") (eq . "RWX") }} {{- $rwxOk = true }} {{- end }} {{- end }} {{- if not $rwxOk }} {{- fail "persistence.mode=shared create.accessModes must include ReadWriteMany for multi-replica" }} {{- end }} {{- end }} apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "opengist.fullname" . }} namespace: {{ .Values.namespace | default .Release.Namespace }} labels: {{- include "opengist.labels" . | nindent 4 }} {{- if .Values.deployment.labels }} {{- toYaml .Values.deployment.labels | nindent 4 }} {{- end }} {{- with .Values.deployment.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: replicas: {{ .Values.replicaCount }} serviceName: {{ include "opengist.fullname" . }}-http podManagementPolicy: {{ .Values.statefulSet.podManagementPolicy }} updateStrategy: {{- toYaml .Values.statefulSet.updateStrategy | nindent 2 }} selector: matchLabels: {{- include "opengist.selectorLabels" . | nindent 6 }} template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "opengist.labels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} spec: {{- if .Values.deployment.terminationGracePeriodSeconds }} terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }} {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "opengist.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} initContainers: - name: init-config image: busybox:1.37 imagePullPolicy: IfNotPresent command: ['sh', '-c', 'cp /init/config/config.yml /config-volume/config.yml'] volumeMounts: - name: config-secret mountPath: /init/config - name: config-volume mountPath: /config-volume {{- if .Values.deployment.env }} env: {{- toYaml .Values.deployment.env | nindent 12 }} {{- end }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.service.http.port }} protocol: TCP {{- if .Values.service.ssh.enabled }} - name: ssh containerPort: {{ .Values.service.ssh.port }} protocol: TCP {{- end }} {{- if .Values.livenessProbe.enabled }} livenessProbe: {{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }} httpGet: port: http path: /healthcheck {{- end }} {{- if .Values.readinessProbe.enabled }} readinessProbe: {{- toYaml (omit .Values.readinessProbe "enabled") | nindent 12 }} httpGet: port: http path: /healthcheck {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - name: config-volume mountPath: /config.yml subPath: config.yml - name: opengist-data mountPath: /opengist {{- if gt (len .Values.extraVolumeMounts) 0 }} {{- toYaml .Values.extraVolumeMounts | nindent 12 }} {{- end }} volumes: - name: config-secret secret: secretName: {{ include "opengist.secretName" . }} defaultMode: 511 - name: config-volume emptyDir: {} {{- /* ======================================== VOLUME MOUNTING DECISION TREE ======================================== Priority order: 1. existingClaim (user-provided PVC) → mount directly 2. mode=shared (chart-created PVC) → mount shared PVC 3. mode=perReplica → use volumeClaimTemplates (defined below) 4. persistence disabled → use emptyDir (ephemeral) */}} {{- if .Values.persistence.enabled }} {{- if ne (default "" .Values.persistence.existingClaim) "" }} {{- /* User-provided existing claim: mount directly */}} - name: opengist-data persistentVolumeClaim: claimName: {{ .Values.persistence.existingClaim }} {{- else if eq (default "perReplica" .Values.persistence.mode) "shared" }} {{- /* Chart creates shared PVC (via pvc-shared.yaml), reference by name */}} - name: opengist-data persistentVolumeClaim: claimName: {{ include "opengist.fullname" . }}-{{ default "shared" .Values.persistence.create.nameSuffix }} {{- else if not .Values.persistence.enabled }} - name: opengist-data emptyDir: {} {{- end }} {{- else }} - name: opengist-data emptyDir: {} {{- end }} {{- if gt (len .Values.extraVolumes) 0 }} {{- toYaml .Values.extraVolumes | nindent 8 }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- /* ======================================== VOLUMECLAIMTEMPLATES DECISION TREE ======================================== volumeClaimTemplates are ONLY used for perReplica mode when: - persistence.enabled=true - persistence.existingClaim is empty - persistence.mode=perReplica (default) This creates one PVC per replica (RWO typically). NOT used when: - existingClaim is set (PVC already exists, referenced in volumes above) - mode=shared (standalone PVC created via pvc-shared.yaml) - persistence disabled (emptyDir used) WARNING: perReplica + replicaCount>1 causes data divergence. Use shared mode for multi-replica. */}} {{- if and .Values.persistence.enabled (ne (default "" .Values.persistence.existingClaim) "") }} {{- /* existingClaim path: no volumeClaimTemplates, already mounted above */}} {{- else if and .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "shared") }} {{- /* shared mode: no volumeClaimTemplates, standalone PVC rendered via pvc-shared.yaml */}} {{- else if and .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "perReplica") }} volumeClaimTemplates: - metadata: name: opengist-data labels: {{- include "opengist.labels" . | nindent 10 }} {{- with .Values.persistence.annotations }} annotations: {{- toYaml . | nindent 10 }} {{- end }} spec: accessModes: {{- .Values.persistence.accessModes | toYaml | nindent 10 }} volumeMode: Filesystem {{- if .Values.persistence.storageClass }} storageClassName: {{ .Values.persistence.storageClass | quote }} {{- end }} resources: requests: storage: {{ .Values.persistence.size | default "10Gi" }} {{- end }} {{- end }}