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:
committed by
GitHub
parent
03420e4f91
commit
4ae25144a0
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Opengist Helm Chart
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
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 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 `<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`
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
48
helm/opengist/templates/pvc-shared.yaml
Normal file
48
helm/opengist/templates/pvc-shared.yaml
Normal 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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
262
helm/opengist/templates/statefulset.yaml
Normal file
262
helm/opengist/templates/statefulset.yaml
Normal 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 }}
|
||||
@@ -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: []
|
||||
|
||||
Reference in New Issue
Block a user