From 4ae25144a01f14e3353fd4ea07ee54c7a3f25392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillem=20Riera=20Galm=C3=A9s?= Date: Wed, 21 Jan 2026 02:22:44 +0100 Subject: [PATCH] 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 --- helm/opengist/Chart.yaml | 2 +- helm/opengist/README.md | 301 ++++++++++++++++++++++- helm/opengist/templates/deployment.yaml | 3 + helm/opengist/templates/pvc-shared.yaml | 48 ++++ helm/opengist/templates/pvc.yaml | 4 +- helm/opengist/templates/secret.yaml | 51 +++- helm/opengist/templates/statefulset.yaml | 262 ++++++++++++++++++++ helm/opengist/values.yaml | 86 ++++++- 8 files changed, 746 insertions(+), 11 deletions(-) create mode 100644 helm/opengist/templates/pvc-shared.yaml create mode 100644 helm/opengist/templates/statefulset.yaml diff --git a/helm/opengist/Chart.yaml b/helm/opengist/Chart.yaml index 1ea24ee..ce750b5 100644 --- a/helm/opengist/Chart.yaml +++ b/helm/opengist/Chart.yaml @@ -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 diff --git a/helm/opengist/README.md b/helm/opengist/README.md index e121b3e..6d43360 100644 --- a/helm/opengist/README.md +++ b/helm/opengist/README.md @@ -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: `. +* 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://:@-postgresql:/` 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 per‑pod 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 re‑deploy. +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 `-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` diff --git a/helm/opengist/templates/deployment.yaml b/helm/opengist/templates/deployment.yaml index 0ef5a36..2983984 100644 --- a/helm/opengist/templates/deployment.yaml +++ b/helm/opengist/templates/deployment.yaml @@ -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 }} diff --git a/helm/opengist/templates/pvc-shared.yaml b/helm/opengist/templates/pvc-shared.yaml new file mode 100644 index 0000000..fb12781 --- /dev/null +++ b/helm/opengist/templates/pvc-shared.yaml @@ -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 }} diff --git a/helm/opengist/templates/pvc.yaml b/helm/opengist/templates/pvc.yaml index 94f2160..99bb582 100644 --- a/helm/opengist/templates/pvc.yaml +++ b/helm/opengist/templates/pvc.yaml @@ -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 }} \ No newline at end of file +{{- end }} diff --git a/helm/opengist/templates/secret.yaml b/helm/opengist/templates/secret.yaml index ac1fe3f..e1ca474 100644 --- a/helm/opengist/templates/secret.yaml +++ b/helm/opengist/templates/secret.yaml @@ -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 }} \ No newline at end of file diff --git a/helm/opengist/templates/statefulset.yaml b/helm/opengist/templates/statefulset.yaml new file mode 100644 index 0000000..60034a6 --- /dev/null +++ b/helm/opengist/templates/statefulset.yaml @@ -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 }} diff --git a/helm/opengist/values.yaml b/helm/opengist/values.yaml index d06c7b8..ac5eee4 100644 --- a/helm/opengist/values.yaml +++ b/helm/opengist/values.yaml @@ -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: -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: []