Adds StatefulSet support (#549)

* Adds StatefulSet support

# Conflicts:
#	helm/opengist/templates/pvc.yaml

* Adds statefulset support for replicaCount gt 1

* Improves the setup of multiple replicas in a stateful set

* Adds config wrangling logic to the secret template

* Adds shared PV functionality

* Adds missing pvc-shared template

* Adds stateful set and documentation

---------

Co-authored-by: Guillem Riera <guillem@rieragalm.es>
This commit is contained in:
Guillem Riera Galmés
2026-01-21 02:22:44 +01:00
committed by GitHub
parent 03420e4f91
commit 4ae25144a0
8 changed files with 746 additions and 11 deletions

View File

@@ -2,7 +2,7 @@ apiVersion: v2
name: opengist
description: Opengist Helm chart for Kubernetes
type: application
version: 0.4.0
version: 0.5.0
appVersion: 1.11.1
home: https://opengist.io
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg

View File

@@ -1,6 +1,6 @@
# Opengist Helm Chart
![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![AppVersion: 1.11.1](https://img.shields.io/badge/AppVersion-1.11.1-informational?style=flat-square)
![Version: 0.5.0](https://img.shields.io/badge/Version-0.5.0-informational?style=flat-square) ![AppVersion: 1.11.1](https://img.shields.io/badge/AppVersion-1.11.1-informational?style=flat-square)
Opengist Helm chart for Kubernetes.
@@ -66,6 +66,40 @@ index.meili.api-key: MASTER_KEY # generated by Meilisearch
If you want to use the `bleve` indexer, you need to set the `replicas` to `1`.
#### Passing Meilisearch configuration via nested Helm values
When using the Helm CLI with `--set`, avoid mixing a scalar `config.index` value with nested `config.index.meili.*` keys. Instead use a nested map and a `type` field which the chart flattens automatically. Example:
```bash
helm template opengist ./helm/opengist \
--set statefulSet.enabled=true \
--set replicaCount=2 \
--set persistence.enabled=true \
--set persistence.existingClaim=opengist-shared-rwx \
--set postgresql.enabled=false \
--set config.db-uri="postgres://user:pass@db-host:5432/opengist" \
--set meilisearch.enabled=true \
--set config.index.type=meilisearch \
--set config.index.meili.host="http://opengist-meilisearch:7700" \
--set config.index.meili.api-key="MASTER_KEY"
```
Rendered `config.yml` fragment:
```yaml
index: meilisearch
index.meili.host: http://opengist-meilisearch:7700
index.meili.api-key: MASTER_KEY
```
How it works:
* You provide a map under `config.index` with keys `type` and `meili`.
* The template detects `config.index.type` and rewrites `index: <type>`.
* Nested `config.index.meili.host` / `api-key` are lifted to flat keys `index.meili.host` and `index.meili.api-key` required by Opengist.
If you set `--set config.index=meilisearch` directly and also try to set `--set config.index.meili.host=...`, Helm will first create the nested structure then overwrite it with the scalar, losing the host. Always prefer the `config.index.type` pattern for CLI usage.
### PostgreSQL Database
By default, Opengist uses the `sqlite` database. If needed, this chart also deploys a PostgreSQL instance.
@@ -79,3 +113,268 @@ Then define the connection string in your Opengist config:
db-uri: postgres://user:password@opengist-postgresql:5432/opengist
```
Note: `opengist-postgresql` is the name of the K8S Service deployed by this chart.
### Database Configuration
You can supply an externally managed database connection explicitly via `config.db-uri` (PostgreSQL/MySQL) or enable the bundled PostgreSQL subchart.
Behavior:
* If `postgresql.enabled: true` and `config.db-uri` is omitted, the chart auto-generates:
`postgres://<username>:<password>@<release-name>-postgresql:<port>/<database>` using values under `postgresql.global.postgresql.auth.*`.
* If any of username/password/database are missing, templating fails fast with an error message.
* If you prefer an external database or a different Postgres distribution, set `postgresql.enabled: false` and provide `config.db-uri` yourself.
**Licensing note**: Bitnami's PostgreSQL distribution may have licensing constraints. For strictly open alternatives use an external managed PostgreSQL/MySQL service and disable the subchart.
### Multi-Replica Requirements
Running more than one Opengist replica (Deployment or StatefulSet) requires:
1. Non-SQLite database (`config.db-uri` must start with `postgres://` or `mysql://`).
2. Shared RWX storage if using StatefulSet with `replicaCount > 1` (provide `persistence.existingClaim`). The chart now fails fast if you attempt `replicaCount > 1` without an explicit shared claim to prevent silent data divergence across perpod PVCs.
The chart will fail fast during templating if these conditions are not met when scaling above 1 replica.
Examples:
* External PostgreSQL:
```yaml
postgresql:
enabled: false
config:
db-uri: postgres://user:pass@db-host:5432/opengist
index: meilisearch
statefulSet:
enabled: true
replicaCount: 2
persistence:
existingClaim: opengist-shared-rwx
```
Bundled PostgreSQL (auto db-uri):
```yaml
postgresql:
enabled: true
config:
index: meilisearch
statefulSet:
enabled: true
replicaCount: 2
persistence:
existingClaim: opengist-shared-rwx
```
#### Recovering from an initial misconfiguration
If you previously scaled a StatefulSet above 1 replica **without** an `existingClaim`, each pod received its own PVC and only one held the authoritative `/opengist` data. To consolidate:
1. Scale down to 1 replica (keep the pod with the desired data):
```bash
kubectl scale sts/opengist --replicas=1
```
1. (Optional) Inspect other PVCs and manually copy any missing files by temporarily attaching them to a debug pod.
1. Create or provision a ReadWriteMany (NFS / CephFS / Longhorn RWX / etc.) PersistentVolumeClaim named (for example) `opengist-shared-rwx`.
1. Update values with `persistence.existingClaim: opengist-shared-rwx` and redeploy.
1. Scale back up:
```bash
kubectl scale sts/opengist --replicas=2
```
Going forward, all replicas mount the same shared volume and data remains consistent.
### Quick Start Examples
Common deployment scenarios with copy-paste configurations:
#### Scenario 1: Single replica with SQLite (default)
Minimal local development setup with ephemeral or persistent storage:
```yaml
# Ephemeral (emptyDir)
statefulSet:
enabled: true
replicaCount: 1
persistence:
enabled: false
# OR with persistent RWO storage
statefulSet:
enabled: true
replicaCount: 1
persistence:
enabled: true
mode: perReplica # default
```
#### Scenario 2: Multi-replica with external PostgreSQL + existing RWX PVC
Production HA setup with your own database and storage:
```yaml
statefulSet:
enabled: true
replicaCount: 2
postgresql:
enabled: false
config:
db-uri: "postgres://user:pass@db-host:5432/opengist"
index: meilisearch # required for multi-replica
persistence:
enabled: true
mode: shared
existingClaim: "opengist-shared-rwx" # pre-created RWX PVC
meilisearch:
enabled: true
```
#### Scenario 3: Multi-replica with bundled PostgreSQL + auto-created RWX PVC
Chart manages both database and storage:
```yaml
statefulSet:
enabled: true
replicaCount: 2
postgresql:
enabled: true
global:
postgresql:
auth:
username: opengist
password: changeme
database: opengist
config:
index: meilisearch
persistence:
enabled: true
mode: shared
existingClaim: "" # empty to trigger auto-creation
create:
enabled: true
accessModes: [ReadWriteMany]
storageClass: "nfs-client" # your RWX-capable storage class
size: 20Gi
meilisearch:
enabled: true
```
### Persistence Modes
The chart supports two persistence strategies controlled by `persistence.mode`:
| Mode | Behavior | Scaling | Storage Objects | Recommended Use |
|-------------|----------|---------|-----------------|-----------------|
| `perReplica` (default) | One PVC per pod via StatefulSet `volumeClaimTemplates` (RWO) when no `existingClaim` | Safe only at `replicaCount=1` unless you supply `existingClaim` | One PVC per replica | Local dev, quick single-node trials |
| `shared` | Single RWX PVC (existing or auto-created) mounted by all pods | Horizontally scalable | One shared PVC | Production / HA |
Configuration examples:
Per-replica (single node):
```yaml
statefulSet:
enabled: true
persistence:
mode: perReplica
enabled: true
accessModes:
- ReadWriteOnce
```
Shared (scale ready) with an existing RWX claim:
```yaml
statefulSet:
enabled: true
replicaCount: 2
persistence:
mode: shared
existingClaim: opengist-shared-rwx
```
Shared with chart-created RWX PVC:
```yaml
statefulSet:
enabled: true
replicaCount: 2
persistence:
mode: shared
existingClaim: "" # leave empty
create:
enabled: true
accessModes: [ReadWriteMany]
size: 10Gi
```
When `mode=shared` and `existingClaim` is empty, the chart creates a single PVC named `<release>-shared` (suffix configurable via `persistence.create.nameSuffix`).
Fail-fast conditions:
* `replicaCount>1` & missing external DB (still enforced).
* `replicaCount>1` & persistence disabled.
* `replicaCount>1` & neither `existingClaim` nor `mode=shared`.
* `mode=shared` & create.enabled=true but `accessModes` lacks `ReadWriteMany`.
Migration (perReplica → shared): scale down to 1, create RWX claim (or rely on create.enabled), copy data, switch mode to shared, scale up.
### Troubleshooting
#### Common Errors and Solutions
##### Error: "replicaCount=2 requires PostgreSQL/MySQL config.db-uri; scheme 'sqlite' unsupported"
* **Cause**: Multi-replica with SQLite database
* **Solution**: Either scale down to `replicaCount: 1` or configure external database:
```yaml
config:
db-uri: "postgres://user:pass@host:5432/opengist"
```
##### Error: "replicaCount=2 requires either persistence.existingClaim OR persistence.mode=shared"
* **Cause**: Multi-replica without shared storage
* **Solution**: Choose one approach:
```yaml
# Option A: Use existing PVC
persistence:
existingClaim: "my-rwx-pvc"
# Option B: Let chart create PVC
persistence:
mode: shared
create:
enabled: true
accessModes: [ReadWriteMany]
```
##### Error: "persistence.mode=shared create.accessModes must include ReadWriteMany for multi-replica"
* **Cause**: Chart-created PVC lacks RWX access mode
* **Solution**: Ensure RWX is specified:
```yaml
persistence:
create:
accessModes:
- ReadWriteMany
```
##### Pods mount different data (data divergence)
* **Cause**: Previously scaled with `perReplica` mode and `replicaCount > 1`
* **Solution**: Follow recovery steps in "Recovering from an initial misconfiguration" section above
##### PVC creation fails: "no storage class available with ReadWriteMany"
* **Cause**: Cluster lacks RWX-capable storage provisioner
* **Solution**: Install a storage provider (NFS, CephFS, Longhorn) or use external managed storage and provide `existingClaim`

View File

@@ -1,3 +1,4 @@
{{- if not .Values.statefulSet.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -124,3 +125,5 @@ spec:
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,48 @@
{{- /*
This template creates a standalone PersistentVolumeClaim for shared persistence mode.
Rendering conditions:
- statefulSet.enabled=true
- persistence.enabled=true
- persistence.mode=shared
- persistence.existingClaim is empty/unset
- persistence.create.enabled=true
When rendered, this PVC is mounted by ALL replicas in the StatefulSet (typically with ReadWriteMany
access mode for multi-replica deployments). This avoids per-replica volumeClaimTemplates and enables
horizontal scaling with a single shared storage backend.
If persistence.existingClaim is set, this template does NOT render; the StatefulSet instead references
the existing claim name directly.
*/}}
{{- if and .Values.statefulSet.enabled .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "shared") (ne (default "" .Values.persistence.existingClaim) "") | not }}{{- end }}
{{- if and .Values.statefulSet.enabled .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "shared") (eq (default "" .Values.persistence.existingClaim) "") .Values.persistence.create.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "opengist.fullname" . }}-{{ default "shared" .Values.persistence.create.nameSuffix }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- with .Values.persistence.create.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.persistence.create.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- if .Values.persistence.create.accessModes }}
{{- toYaml .Values.persistence.create.accessModes | nindent 4 }}
{{- else }}
- ReadWriteMany
{{- end }}
resources:
requests:
storage: {{ default .Values.persistence.size .Values.persistence.create.size }}
volumeMode: Filesystem
{{- $sc := default .Values.persistence.storageClass .Values.persistence.create.storageClass }}
{{- if $sc }}
storageClassName: {{ $sc | quote }}
{{- end }}
{{- end }}

View File

@@ -1,4 +1,4 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
{{- if and .Values.persistence.enabled (not .Values.statefulSet.enabled) (not .Values.persistence.existingClaim) }}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
@@ -25,4 +25,4 @@ spec:
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
{{- end }}

View File

@@ -1,4 +1,53 @@
{{- if (not .Values.configExistingSecret) }}
{{- $cfg := deepCopy .Values.config }}
{{- /* Backward compatibility: map db-uri (deprecated) to db-uri key still expected by app, also accept dbUri coming from user */}}
{{- if and (hasKey $cfg "dbUri") (not (hasKey $cfg "db-uri")) }}
{{- $_ := set $cfg "db-uri" (index $cfg "dbUri") }}
{{- end }}
{{- $dburi := default "" (index $cfg "db-uri") }}
{{- /* Flatten possible nested index.meili.* structure if user passed --set config.index.meili.host=... */}}
{{- if and (hasKey $cfg "index") (kindIs "map" (index $cfg "index")) }}
{{- $indexMap := (index $cfg "index") }}
{{- if hasKey $indexMap "type" }}
{{- $_ := set $cfg "index" (index $indexMap "type") }}
{{- end }}
{{- if hasKey $indexMap "meili" }}
{{- $meili := (index $indexMap "meili") }}
{{- if hasKey $meili "host" }}
{{- $_ := set $cfg "index.meili.host" (index $meili "host") }}
{{- end }}
{{- if hasKey $meili "api-key" }}
{{- $_ := set $cfg "index.meili.api-key" (index $meili "api-key") }}
{{- end }}
{{- end }}
{{- end }}
{{- if and .Values.postgresql.enabled (eq $dburi "") }}
{{- $user := default "" .Values.postgresql.global.postgresql.auth.username }}
{{- $pass := default "" .Values.postgresql.global.postgresql.auth.password }}
{{- $db := default "" .Values.postgresql.global.postgresql.auth.database }}
{{- $port := default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql }}
{{- if or (eq $user "") (eq $pass "") (eq $db "") }}
{{- fail "postgresql.enabled=true requires username/password/database (postgresql.global.postgresql.auth.*) or set config.db-uri manually" }}
{{- end }}
{{- $autoHost := printf "%s-postgresql" (include "opengist.fullname" .) }}
{{- $autoUri := printf "postgres://%s:%s@%s:%d/%s" $user $pass $autoHost $port $db }}
{{- $_ := set $cfg "db-uri" $autoUri }}
{{- end }}
{{- $replicas := int .Values.replicaCount }}
{{- $index := default "" (index $cfg "index") }}
{{- /* Auto-set Meilisearch host if subchart enabled and host missing */}}
{{- $meiliHost := default "" (index $cfg "index.meili.host") }}
{{- if and .Values.meilisearch.enabled (eq $meiliHost "") }}
{{- $autoMeiliHost := printf "http://%s-meilisearch:7700" (include "opengist.fullname" .) }}
{{- $_ := set $cfg "index.meili.host" $autoMeiliHost }}
{{- if or (eq $index "") (ne $index "meilisearch") }}
{{- $_ := set $cfg "index" "meilisearch" }}
{{- $index = "meilisearch" }}
{{- end }}
{{- end }}
{{- if and (gt $replicas 1) (or (eq $index "") (eq $index "bleve")) }}
{{- fail "replicaCount>1 requires index set to 'meilisearch' (bleve not supported with multiple replicas)" }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
@@ -9,5 +58,5 @@ metadata:
type: Opaque
stringData:
config.yml: |-
{{- .Values.config | toYaml | nindent 4 }}
{{- $cfg | toYaml | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,262 @@
{{- 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 }}

View File

@@ -32,6 +32,34 @@ strategy:
maxSurge: "100%"
maxUnavailable: 0
## StatefulSet configuration
## Enables StatefulSet workload instead of Deployment (required for volumeClaimTemplates or stable pod identities).
##
## Single-replica SQLite example (default behavior):
## statefulSet.enabled: true
## replicaCount: 1
## persistence.mode: perReplica # or omit (default)
## # Creates one PVC per pod via volumeClaimTemplates (RWO)
##
## Multi-replica requirements (replicaCount > 1):
## 1. External database: config.db-uri must be postgres:// or mysql:// (SQLite NOT supported)
## 2. Shared storage: Use ONE of:
## a) Existing claim: persistence.existingClaim: "my-rwx-pvc"
## b) Chart-created: persistence.mode: shared + persistence.create.enabled: true + accessModes: [ReadWriteMany]
## 3. Chart will FAIL FAST if constraints are not met to prevent data divergence
##
## Persistence decision tree:
## - persistence.existingClaim set → mount that PVC directly (no volumeClaimTemplates)
## - persistence.mode=shared + create.* → chart creates single RWX PVC, all pods mount it
## - persistence.mode=perReplica (default) → volumeClaimTemplates (one PVC/pod, RWO typically)
## - persistence.enabled=false → emptyDir (ephemeral)
statefulSet:
enabled: false
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
## Security Context settings
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
podSecurityContext:
@@ -99,20 +127,66 @@ serviceAccount:
annotations: {}
name: ""
## Set persistence using a Persistent Volume Claim
## If more than 2 replicas are set, the access mode must be ReadWriteMany
## Persistent storage for /opengist data directory
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
persistence:
enabled: true
## Persistence mode controls how storage is provisioned:
##
## perReplica (DEFAULT):
## - StatefulSet creates one PVC per replica via volumeClaimTemplates
## - Typically RWO (ReadWriteOnce) storage
## - Safe ONLY for replicaCount=1 (multi-replica causes data divergence)
## - Use when: single-node dev/test, no horizontal scaling needed
##
## shared:
## - Single RWX (ReadWriteMany) PVC shared by all replicas
## - Required for replicaCount > 1
## - Two provisioning paths:
## a) existingClaim: "my-rwx-pvc" (you manage the PVC lifecycle)
## b) existingClaim: "" + create.enabled: true (chart creates PVC automatically)
## - Use when: multi-replica HA, horizontal scaling, shared file access
##
## WARNING: Switching modes after initial deploy requires manual data migration:
## 1. Scale down to 1 replica
## 2. Create/provision RWX PVC and copy data
## 3. Update values: mode=shared, existingClaim or create.enabled
## 4. Scale up
mode: perReplica
## Reference an existing PVC (takes precedence over create.*)
## When set:
## - Chart will NOT create a PVC
## - StatefulSet mounts this claim directly (no volumeClaimTemplates)
## - Must be RWX for replicaCount > 1
## Example: existingClaim: "opengist-shared-rwx"
existingClaim: ""
storageClass: ""
## Common persistence parameters (apply to perReplica mode OR as defaults for create.*)
storageClass: "" # Empty = cluster default
labels: {}
annotations:
helm.sh/resource-policy: keep
helm.sh/resource-policy: keep # Prevents PVC deletion on helm uninstall
size: 5Gi
accessModes:
- ReadWriteOnce
subPath: ""
- ReadWriteOnce # perReplica default; override to [ReadWriteMany] if using existingClaim
subPath: "" # Optional subpath within volume
## Chart-managed PVC creation (ONLY for mode=shared when existingClaim is empty)
## Renders templates/pvc-shared.yaml
create:
enabled: true
nameSuffix: shared # PVC name: <release-name>-shared
storageClass: "" # Empty = cluster default; override if you need specific storage class
size: 5Gi # Override top-level persistence.size if needed
accessModes:
- ReadWriteMany # REQUIRED for multi-replica; NFS/CephFS/Longhorn RWX/etc.
labels: {}
annotations: {}
## Example for specific storage:
## storageClass: "nfs-client"
## size: 20Gi
extraVolumes: []
extraVolumeMounts: []