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
|
name: opengist
|
||||||
description: Opengist Helm chart for Kubernetes
|
description: Opengist Helm chart for Kubernetes
|
||||||
type: application
|
type: application
|
||||||
version: 0.4.0
|
version: 0.5.0
|
||||||
appVersion: 1.11.1
|
appVersion: 1.11.1
|
||||||
home: https://opengist.io
|
home: https://opengist.io
|
||||||
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
|
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Opengist Helm Chart
|
# Opengist Helm Chart
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
Opengist Helm chart for Kubernetes.
|
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`.
|
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
|
### PostgreSQL Database
|
||||||
|
|
||||||
By default, Opengist uses the `sqlite` database. If needed, this chart also deploys a PostgreSQL instance.
|
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
|
db-uri: postgres://user:password@opengist-postgresql:5432/opengist
|
||||||
```
|
```
|
||||||
Note: `opengist-postgresql` is the name of the K8S Service deployed by this chart.
|
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
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -124,3 +125,5 @@ spec:
|
|||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- 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
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -1,4 +1,53 @@
|
|||||||
{{- if (not .Values.configExistingSecret) }}
|
{{- 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
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
@@ -9,5 +58,5 @@ metadata:
|
|||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
config.yml: |-
|
config.yml: |-
|
||||||
{{- .Values.config | toYaml | nindent 4 }}
|
{{- $cfg | toYaml | nindent 4 }}
|
||||||
{{- end }}
|
{{- 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%"
|
maxSurge: "100%"
|
||||||
maxUnavailable: 0
|
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
|
## Security Context settings
|
||||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||||
podSecurityContext:
|
podSecurityContext:
|
||||||
@@ -99,20 +127,66 @@ serviceAccount:
|
|||||||
annotations: {}
|
annotations: {}
|
||||||
name: ""
|
name: ""
|
||||||
|
|
||||||
## Set persistence using a Persistent Volume Claim
|
## Persistent storage for /opengist data directory
|
||||||
## If more than 2 replicas are set, the access mode must be ReadWriteMany
|
|
||||||
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
|
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
|
||||||
persistence:
|
persistence:
|
||||||
enabled: true
|
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: ""
|
existingClaim: ""
|
||||||
storageClass: ""
|
|
||||||
|
## Common persistence parameters (apply to perReplica mode OR as defaults for create.*)
|
||||||
|
storageClass: "" # Empty = cluster default
|
||||||
labels: {}
|
labels: {}
|
||||||
annotations:
|
annotations:
|
||||||
helm.sh/resource-policy: keep
|
helm.sh/resource-policy: keep # Prevents PVC deletion on helm uninstall
|
||||||
size: 5Gi
|
size: 5Gi
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce # perReplica default; override to [ReadWriteMany] if using existingClaim
|
||||||
subPath: ""
|
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: []
|
extraVolumes: []
|
||||||
extraVolumeMounts: []
|
extraVolumeMounts: []
|
||||||
|
|||||||
Reference in New Issue
Block a user