diff --git a/Changelog.md b/Changelog.md index a6fc411..fb12676 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,24 @@ # Changelog +## 1.1.0 ![AppVersion: 30.1](https://img.shields.io/static/v1?label=AppVersion&message=30.1&color=success&logo=) ![Helm: v3](https://img.shields.io/static/v1?label=Helm&message=v3&color=informational&logo=helm) + +**Release date:** 2026-03-20 + +### Added + +- PodDisruptionBudget is now enabled by default for multi-node clusters (replicaCount > 1), automatically skipped for single-replica deployments +- Auto-calculated `maxUnavailable` for both PDB and StatefulSet `updateStrategy`, based on Raft fault tolerance (`floor(replicaCount/2)`), with user override support and quorum validation +- Explicit `updateStrategy` on StatefulSet with `RollingUpdate` default and `OnDelete` support +- Built-in soft pod anti-affinity (`preferredDuringSchedulingIgnoredDuringExecution`) on `kubernetes.io/hostname` for multi-node clusters, automatically spreading replicas across nodes +- Template-level validation that rejects `maxUnavailable` values exceeding Raft fault tolerance + +### Changed + +- `pdb.enabled` default changed from `false` to `true` +- `pdb.maxUnavailable` default changed from `1` to `"auto"` (auto-calculate as `floor(replicaCount/2)`) +- `updateStrategy.rollingUpdate.maxUnavailable` added with default `"auto"` (auto-calculate) +- Built-in soft pod anti-affinity is now injected when `affinity` is empty (`{}`) + ## 1.0.0 ![AppVersion: 30.1](https://img.shields.io/static/v1?label=AppVersion&message=30.1&color=success&logo=) ![Helm: v3](https://img.shields.io/static/v1?label=Helm&message=v3&color=informational&logo=helm) **Release date:** 2026-03-19 diff --git a/Chart.yaml b/Chart.yaml index e553b69..e5c1d32 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,11 +1,9 @@ apiVersion: v2 name: typesense description: >- - Deploy Typesense search engine on Kubernetes with Raft-based HA clustering, - Prometheus metrics, and Gateway API support. This chart is not officially - maintained by or affiliated with the Typesense project. + Deploy Typesense search engine on Kubernetes with Raft-based HA clustering, Prometheus metrics, and Gateway API support. This chart is not officially maintained by or affiliated with the Typesense project. type: application -version: 1.0.0 +version: 1.1.0 appVersion: "30.1" icon: https://typesense.org/typesense-logo.svg home: https://github.com/hackthebox/typesense-helm diff --git a/README.md b/README.md index 5efcc50..4e061ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # typesense -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/typesense)](https://artifacthub.io/packages/search?repo=typesense) @@ -217,7 +217,7 @@ storage: | Key | Type | Default | Description | |-----|------|---------|-------------| -| affinity | object | `{}` | Affinity rules for pod scheduling | +| affinity | object | `{}` | Affinity rules for pod scheduling. When unset or empty and replicaCount > 1, a soft pod anti-affinity on kubernetes.io/hostname is automatically applied. Set to a non-empty affinity object to override this default behavior. | | extraArgs | list | `[]` | Extra command-line arguments for Typesense server (e.g., ["--filter-by-max-ops=200"]) | | extraEnv | list | `[]` | Extra environment variables for the Typesense container | | fullnameOverride | string | `""` | Override the full name of the release (optional) | @@ -247,8 +247,8 @@ storage: | metrics.serviceMonitor.labels | object | `{}` | Additional labels for ServiceMonitor | | nameOverride | string | `""` | Override the name of the release (optional) | | nodeSelector | object | `{}` | Node selector to schedule pods on specific nodes (optional) | -| pdb.enabled | bool | `false` | Enable PodDisruptionBudget for Typesense StatefulSet | -| pdb.maxUnavailable | int | `1` | Maximum number of pods that can be unavailable during disruption | +| pdb.enabled | bool | `true` | Enable PodDisruptionBudget for Typesense StatefulSet. Automatically skipped when replicaCount is 1. | +| pdb.maxUnavailable | string/int | `"auto"` | Maximum number of pods that can be unavailable during disruption. Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary disruptions. Any positive value is used directly and must not exceed floor(replicaCount/2). | | podAnnotations | object | `{}` | Additional annotations to add to the Typesense pod(s) | | podLabels | object | `{}` | Additional labels to add to the Typesense pod(s) | | podSecurityContext.fsGroup | int | `2000` | Group ID for the filesystem of the Typesense container | @@ -295,6 +295,9 @@ storage: | typesense.logging.slowRequestsTimeMs | string | `nil` | Threshold in ms for slow request logging (-1 disables). Unset uses Typesense default (-1) | | typesense.snapshots.intervalSeconds | string | `nil` | Replication log snapshot frequency in seconds. Unset uses Typesense default (3600) | | typesense.threadPoolSize | string | `nil` | Concurrent request handler threads. Unset uses Typesense default (NUM_CORES * 8) | +| updateStrategy | object | `{"rollingUpdate":{"maxUnavailable":"auto"},"type":"RollingUpdate"}` | StatefulSet update strategy configuration. When type is RollingUpdate, rollingUpdate.maxUnavailable is required. When type is OnDelete, rollingUpdate is optional and ignored. | +| updateStrategy.rollingUpdate.maxUnavailable | string/int | `"auto"` | Maximum number of pods that can be unavailable during a rolling update. Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary pod replacements. Any positive value is used directly and must not exceed floor(replicaCount/2). Ignored when updateStrategy.type is OnDelete. | +| updateStrategy.type | string | `"RollingUpdate"` | StatefulSet update strategy type. Use RollingUpdate (default) for zero-downtime upgrades or OnDelete for manual pod-by-pod control. | ## Upgrading diff --git a/templates/pdb.yaml b/templates/pdb.yaml index e61a137..79667b3 100644 --- a/templates/pdb.yaml +++ b/templates/pdb.yaml @@ -1,4 +1,15 @@ -{{- if .Values.pdb.enabled -}} +{{- if and .Values.pdb.enabled (gt (int .Values.replicaCount) 1) -}} +{{- $replicaCount := int .Values.replicaCount -}} +{{- $maxFault := div $replicaCount 2 -}} +{{- $maxUnavailable := .Values.pdb.maxUnavailable -}} +{{- if and (kindIs "string" $maxUnavailable) (eq $maxUnavailable "auto") }} + {{- $maxUnavailable = $maxFault -}} +{{- else }} + {{- $maxUnavailable = int $maxUnavailable -}} + {{- if gt $maxUnavailable $maxFault }} +{{- fail (printf "pdb.maxUnavailable (%d) exceeds Raft fault tolerance for replicaCount %d (max: floor(%d/2) = %d)" $maxUnavailable $replicaCount $replicaCount $maxFault) }} + {{- end }} +{{- end }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -7,7 +18,7 @@ metadata: labels: {{- include "typesense.labels" . | nindent 4 }} spec: - maxUnavailable: {{ .Values.pdb.maxUnavailable }} + maxUnavailable: {{ $maxUnavailable }} selector: matchLabels: {{- include "typesense.selectorLabels" . | nindent 6 }} diff --git a/templates/statefulset.yaml b/templates/statefulset.yaml index 5b22c57..8ef0461 100644 --- a/templates/statefulset.yaml +++ b/templates/statefulset.yaml @@ -13,6 +13,30 @@ spec: serviceName: {{ include "typesense.fullname" . }}-headless-svc podManagementPolicy: Parallel replicas: {{ .Values.replicaCount }} + updateStrategy: + {{- if not (or (eq .Values.updateStrategy.type "RollingUpdate") (eq .Values.updateStrategy.type "OnDelete")) }} + {{- fail (printf "updateStrategy.type must be RollingUpdate or OnDelete, got %q" .Values.updateStrategy.type) }} + {{- end }} + type: {{ .Values.updateStrategy.type }} + {{- if eq .Values.updateStrategy.type "RollingUpdate" }} + {{- $replicaCount := int .Values.replicaCount }} + {{- $maxFault := div $replicaCount 2 }} + {{- $maxUnavailable := .Values.updateStrategy.rollingUpdate.maxUnavailable }} + {{- if and (kindIs "string" $maxUnavailable) (eq $maxUnavailable "auto") }} + {{- if gt $replicaCount 1 }} + {{- $maxUnavailable = $maxFault }} + {{- else }} + {{- $maxUnavailable = 1 }} + {{- end }} + {{- else }} + {{- $maxUnavailable = int $maxUnavailable }} + {{- if and (gt $replicaCount 1) (gt $maxUnavailable $maxFault) }} + {{- fail (printf "updateStrategy.rollingUpdate.maxUnavailable (%d) exceeds Raft fault tolerance for replicaCount %d (max: floor(%d/2) = %d)" $maxUnavailable $replicaCount $replicaCount $maxFault) }} + {{- end }} + {{- end }} + rollingUpdate: + maxUnavailable: {{ $maxUnavailable }} + {{- end }} selector: matchLabels: {{- include "typesense.selectorLabels" . | nindent 6 }} @@ -155,9 +179,19 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.affinity }} + {{- if .Values.affinity }} affinity: - {{- toYaml . | nindent 8 }} + {{- toYaml .Values.affinity | nindent 8 }} + {{- else if gt (int .Values.replicaCount) 1 }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "typesense.selectorLabels" . | nindent 20 }} + topologyKey: kubernetes.io/hostname {{- end }} {{- with .Values.topologySpreadConstraints }} topologySpreadConstraints: diff --git a/tests/config_test.yaml b/tests/config_test.yaml index 711a68a..1c24c4c 100644 --- a/tests/config_test.yaml +++ b/tests/config_test.yaml @@ -1,6 +1,4 @@ suite: Configuration -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/external_secret_test.yaml b/tests/external_secret_test.yaml index aff57b2..58eed3f 100644 --- a/tests/external_secret_test.yaml +++ b/tests/external_secret_test.yaml @@ -1,6 +1,4 @@ suite: Secrets -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/ingress_test.yaml b/tests/ingress_test.yaml index d16b621..2abc0a8 100644 --- a/tests/ingress_test.yaml +++ b/tests/ingress_test.yaml @@ -1,6 +1,4 @@ suite: Networking -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/pdb_test.yaml b/tests/pdb_test.yaml index e08b958..ef0455a 100644 --- a/tests/pdb_test.yaml +++ b/tests/pdb_test.yaml @@ -1,21 +1,11 @@ suite: PodDisruptionBudget -chart: - version: 1.0.0 release: name: foo namespace: bar templates: - pdb.yaml tests: - - it: should not create PDB by default - asserts: - - notFailedTemplate: {} - - hasDocuments: - count: 0 - - it: should create PDB when enabled - set: - pdb: - enabled: true + - it: should create PDB by default with auto-calculated maxUnavailable asserts: - notFailedTemplate: {} - hasDocuments: @@ -28,16 +18,107 @@ tests: - equal: path: .spec.maxUnavailable value: 1 - - it: should allow overriding maxUnavailable + + - it: should not create PDB when replicaCount is 1 + set: + replicaCount: 1 + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 0 + + - it: should not create PDB when replicaCount is 1 even if pdb.enabled is true set: + replicaCount: 1 pdb: enabled: true - maxUnavailable: 2 + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 0 + + - it: should not create PDB when disabled + set: + pdb: + enabled: false + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 0 + + - it: should auto-calculate maxUnavailable as floor(replicaCount/2) for 5 replicas + set: + replicaCount: 5 asserts: - notFailedTemplate: {} - equal: path: .spec.maxUnavailable value: 2 + + - it: should auto-calculate maxUnavailable as floor(replicaCount/2) for 7 replicas + set: + replicaCount: 7 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.maxUnavailable + value: 3 + + - it: should auto-calculate when maxUnavailable is auto + set: + replicaCount: 5 + pdb: + enabled: true + maxUnavailable: auto + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.maxUnavailable + value: 2 + + - it: should allow maxUnavailable 0 to block all voluntary disruptions + set: + pdb: + enabled: true + maxUnavailable: 0 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.maxUnavailable + value: 0 + + - it: should allow overriding maxUnavailable within quorum bounds + set: + replicaCount: 5 + pdb: + enabled: true + maxUnavailable: 1 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.maxUnavailable + value: 1 + + - it: should reject maxUnavailable exceeding Raft fault tolerance + set: + replicaCount: 3 + pdb: + enabled: true + maxUnavailable: 2 + asserts: + - failedTemplate: + errorMessage: "pdb.maxUnavailable (2) exceeds Raft fault tolerance for replicaCount 3 (max: floor(3/2) = 1)" + + - it: should reject maxUnavailable exceeding Raft fault tolerance for 5 replicas + set: + replicaCount: 5 + pdb: + enabled: true + maxUnavailable: 3 + asserts: + - failedTemplate: + errorMessage: "pdb.maxUnavailable (3) exceeds Raft fault tolerance for replicaCount 5 (max: floor(5/2) = 2)" + - it: should use selector labels matching the StatefulSet set: pdb: diff --git a/tests/service_test.yaml b/tests/service_test.yaml index e1c2e5a..f3e8df7 100644 --- a/tests/service_test.yaml +++ b/tests/service_test.yaml @@ -1,6 +1,4 @@ suite: Networking -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/serviceaccount_test.yaml b/tests/serviceaccount_test.yaml index 80d6a07..3b7a486 100644 --- a/tests/serviceaccount_test.yaml +++ b/tests/serviceaccount_test.yaml @@ -1,6 +1,4 @@ suite: ServiceAccount -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/servicemonitor_test.yaml b/tests/servicemonitor_test.yaml index 95f53ca..7a4cf83 100644 --- a/tests/servicemonitor_test.yaml +++ b/tests/servicemonitor_test.yaml @@ -1,6 +1,4 @@ suite: ServiceMonitor -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/statefulset_test.yaml b/tests/statefulset_test.yaml index 3e97f30..685cb18 100644 --- a/tests/statefulset_test.yaml +++ b/tests/statefulset_test.yaml @@ -1,6 +1,4 @@ suite: App -chart: - version: 1.0.0 release: name: foo namespace: bar @@ -23,14 +21,16 @@ tests: - equal: path: .spec.replicas value: 3 - - equal: + - isSubset: path: .spec.template.metadata.labels - value: - helm.sh/chart: typesense-1.0.0 + content: app.kubernetes.io/name: typesense app.kubernetes.io/instance: foo app.kubernetes.io/version: "30.1" app.kubernetes.io/managed-by: Helm + - matchRegex: + path: .spec.template.metadata.labels["helm.sh/chart"] + pattern: ^typesense-\d+\.\d+\.\d+$ - equal: path: .spec.template.spec.securityContext @@ -292,13 +292,13 @@ tests: path: /health port: http periodSeconds: 5 - - it: should not render scheduling fields when empty + - it: should render default anti-affinity but not tolerations or topologySpreadConstraints when empty asserts: - notFailedTemplate: {} + - exists: + path: .spec.template.spec.affinity - notExists: path: .spec.template.spec.tolerations - - notExists: - path: .spec.template.spec.affinity - notExists: path: .spec.template.spec.topologySpreadConstraints - it: should reference the service account @@ -512,3 +512,153 @@ tests: - equal: path: .spec.template.spec.containers[1].envFrom[0].secretRef.name value: foo-typesense-secret + + - it: should render RollingUpdate strategy with auto-calculated maxUnavailable by default + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.type + value: RollingUpdate + - equal: + path: .spec.updateStrategy.rollingUpdate.maxUnavailable + value: 1 + + - it: should auto-calculate updateStrategy maxUnavailable for 5 replicas + set: + replicaCount: 5 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.rollingUpdate.maxUnavailable + value: 2 + + - it: should render maxUnavailable 1 for replicaCount 1 + set: + replicaCount: 1 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.type + value: RollingUpdate + - equal: + path: .spec.updateStrategy.rollingUpdate.maxUnavailable + value: 1 + + - it: should auto-calculate updateStrategy maxUnavailable when set to auto + set: + replicaCount: 5 + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: auto + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.rollingUpdate.maxUnavailable + value: 2 + + - it: should allow updateStrategy maxUnavailable 0 to block all voluntary replacements + set: + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.rollingUpdate.maxUnavailable + value: 0 + + - it: should allow overriding updateStrategy maxUnavailable within quorum bounds + set: + replicaCount: 5 + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.rollingUpdate.maxUnavailable + value: 1 + + - it: should reject updateStrategy maxUnavailable exceeding Raft fault tolerance + set: + replicaCount: 3 + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 2 + asserts: + - failedTemplate: + errorMessage: "updateStrategy.rollingUpdate.maxUnavailable (2) exceeds Raft fault tolerance for replicaCount 3 (max: floor(3/2) = 1)" + + - it: should render OnDelete strategy without rollingUpdate block + set: + updateStrategy: + type: OnDelete + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.updateStrategy.type + value: OnDelete + - notExists: + path: .spec.updateStrategy.rollingUpdate + + - it: should inject default soft anti-affinity for multi-node clusters + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.affinity + value: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: typesense + app.kubernetes.io/instance: foo + topologyKey: kubernetes.io/hostname + + - it: should not inject anti-affinity when replicaCount is 1 + set: + replicaCount: 1 + asserts: + - notFailedTemplate: {} + - notExists: + path: .spec.template.spec.affinity + + - it: should use user-provided affinity instead of default when set + set: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: typesense + topologyKey: kubernetes.io/hostname + asserts: + - notFailedTemplate: {} + - exists: + path: .spec.template.spec.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution + - notExists: + path: .spec.template.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution + + - it: should not inject anti-affinity when user provides node affinity + set: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/os + operator: In + values: + - linux + asserts: + - notFailedTemplate: {} + - exists: + path: .spec.template.spec.affinity.nodeAffinity + - notExists: + path: .spec.template.spec.affinity.podAntiAffinity diff --git a/values.md b/values.md index 5efcc50..4e061ba 100644 --- a/values.md +++ b/values.md @@ -1,6 +1,6 @@ # typesense -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/typesense)](https://artifacthub.io/packages/search?repo=typesense) @@ -217,7 +217,7 @@ storage: | Key | Type | Default | Description | |-----|------|---------|-------------| -| affinity | object | `{}` | Affinity rules for pod scheduling | +| affinity | object | `{}` | Affinity rules for pod scheduling. When unset or empty and replicaCount > 1, a soft pod anti-affinity on kubernetes.io/hostname is automatically applied. Set to a non-empty affinity object to override this default behavior. | | extraArgs | list | `[]` | Extra command-line arguments for Typesense server (e.g., ["--filter-by-max-ops=200"]) | | extraEnv | list | `[]` | Extra environment variables for the Typesense container | | fullnameOverride | string | `""` | Override the full name of the release (optional) | @@ -247,8 +247,8 @@ storage: | metrics.serviceMonitor.labels | object | `{}` | Additional labels for ServiceMonitor | | nameOverride | string | `""` | Override the name of the release (optional) | | nodeSelector | object | `{}` | Node selector to schedule pods on specific nodes (optional) | -| pdb.enabled | bool | `false` | Enable PodDisruptionBudget for Typesense StatefulSet | -| pdb.maxUnavailable | int | `1` | Maximum number of pods that can be unavailable during disruption | +| pdb.enabled | bool | `true` | Enable PodDisruptionBudget for Typesense StatefulSet. Automatically skipped when replicaCount is 1. | +| pdb.maxUnavailable | string/int | `"auto"` | Maximum number of pods that can be unavailable during disruption. Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary disruptions. Any positive value is used directly and must not exceed floor(replicaCount/2). | | podAnnotations | object | `{}` | Additional annotations to add to the Typesense pod(s) | | podLabels | object | `{}` | Additional labels to add to the Typesense pod(s) | | podSecurityContext.fsGroup | int | `2000` | Group ID for the filesystem of the Typesense container | @@ -295,6 +295,9 @@ storage: | typesense.logging.slowRequestsTimeMs | string | `nil` | Threshold in ms for slow request logging (-1 disables). Unset uses Typesense default (-1) | | typesense.snapshots.intervalSeconds | string | `nil` | Replication log snapshot frequency in seconds. Unset uses Typesense default (3600) | | typesense.threadPoolSize | string | `nil` | Concurrent request handler threads. Unset uses Typesense default (NUM_CORES * 8) | +| updateStrategy | object | `{"rollingUpdate":{"maxUnavailable":"auto"},"type":"RollingUpdate"}` | StatefulSet update strategy configuration. When type is RollingUpdate, rollingUpdate.maxUnavailable is required. When type is OnDelete, rollingUpdate is optional and ignored. | +| updateStrategy.rollingUpdate.maxUnavailable | string/int | `"auto"` | Maximum number of pods that can be unavailable during a rolling update. Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary pod replacements. Any positive value is used directly and must not exceed floor(replicaCount/2). Ignored when updateStrategy.type is OnDelete. | +| updateStrategy.type | string | `"RollingUpdate"` | StatefulSet update strategy type. Use RollingUpdate (default) for zero-downtime upgrades or OnDelete for manual pod-by-pod control. | ## Upgrading diff --git a/values.schema.json b/values.schema.json index bfa6c4b..675a0a0 100644 --- a/values.schema.json +++ b/values.schema.json @@ -2,9 +2,10 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "affinity": { - "description": "Affinity rules for pod scheduling", + "description": "Affinity rules for pod scheduling. When unset and replicaCount \u003e 1, a soft pod anti-affinity on kubernetes.io/hostname is automatically applied. Set to a non-empty affinity object to override.", "required": [], - "title": "affinity" + "title": "affinity", + "type": "object" }, "extraArgs": { "description": "Extra command-line arguments for Typesense server (e.g., [\"--filter-by-max-ops=200\"])", @@ -307,17 +308,28 @@ "pdb": { "properties": { "enabled": { - "default": false, - "description": "Enable PodDisruptionBudget for Typesense StatefulSet", + "default": true, + "description": "Enable PodDisruptionBudget for Typesense StatefulSet. Automatically skipped when replicaCount is 1.", "title": "enabled", "type": "boolean" }, "maxUnavailable": { - "default": 1, - "description": "Maximum number of pods that can be unavailable during disruption", - "minimum": 1, - "title": "maxUnavailable", - "type": "integer" + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "enum": [ + "auto" + ], + "required": [] + } + ], + "default": "auto", + "description": "Maximum number of pods that can be unavailable during disruption. Set to \"auto\" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary disruptions. Any positive value is used directly and must not exceed floor(replicaCount/2).", + "required": [], + "title": "maxUnavailable" } }, "required": [], @@ -822,6 +834,68 @@ ], "title": "typesense", "type": "object" + }, + "updateStrategy": { + "description": "StatefulSet update strategy configuration. When type is RollingUpdate, rollingUpdate.maxUnavailable is required. When type is OnDelete, rollingUpdate is optional and ignored.", + "if": { + "properties": { + "type": { + "const": "RollingUpdate", + "required": [] + } + }, + "required": [] + }, + "properties": { + "rollingUpdate": { + "description": "Rolling update configuration. Only used when type is RollingUpdate.", + "properties": { + "maxUnavailable": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "enum": [ + "auto" + ], + "required": [] + } + ], + "description": "Maximum number of pods unavailable during a rolling update. Set to \"auto\" to auto-calculate as floor(replicaCount/2). Set to 0 to block all voluntary pod replacements. Must not exceed floor(replicaCount/2).", + "required": [] + } + }, + "required": [], + "type": "object" + }, + "type": { + "description": "StatefulSet update strategy type. Use RollingUpdate for zero-downtime upgrades or OnDelete for manual pod-by-pod control.", + "enum": [ + "RollingUpdate", + "OnDelete" + ], + "required": [] + } + }, + "required": [ + "type" + ], + "then": { + "properties": { + "rollingUpdate": { + "required": [ + "maxUnavailable" + ] + } + }, + "required": [ + "rollingUpdate" + ] + }, + "title": "updateStrategy", + "type": "object" } }, "required": [ diff --git a/values.yaml b/values.yaml index 2e5e7e0..efc671c 100644 --- a/values.yaml +++ b/values.yaml @@ -156,7 +156,12 @@ readinessProbe: nodeSelector: {} # -- Tolerations for pod scheduling tolerations: [] -# -- Affinity rules for pod scheduling +# @schema +# type: object +# @schema +# -- Affinity rules for pod scheduling. When unset or empty and replicaCount > 1, +# a soft pod anti-affinity on kubernetes.io/hostname is automatically applied. Set +# to a non-empty affinity object to override this default behavior. affinity: {} # -- Topology spread constraints for pod distribution across nodes/zones topologySpreadConstraints: [] @@ -165,14 +170,65 @@ pdb: # @schema # type: boolean # @schema - # -- Enable PodDisruptionBudget for Typesense StatefulSet - enabled: false + # -- Enable PodDisruptionBudget for Typesense StatefulSet. Automatically skipped when replicaCount is 1. + enabled: true # @schema - # type: integer - # minimum: 1 + # anyOf: + # - type: integer + # minimum: 0 + # - enum: [auto] # @schema - # -- Maximum number of pods that can be unavailable during disruption - maxUnavailable: 1 + # -- (string/int) Maximum number of pods that can be unavailable during disruption. + # Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. + # Set to 0 to block all voluntary disruptions. Any positive value is used directly + # and must not exceed floor(replicaCount/2). + maxUnavailable: auto + +# @schema +# type: object +# required: +# - type +# properties: +# type: +# description: "StatefulSet update strategy type. Use RollingUpdate for zero-downtime upgrades or OnDelete for manual pod-by-pod control." +# enum: +# - RollingUpdate +# - OnDelete +# rollingUpdate: +# type: object +# description: "Rolling update configuration. Only used when type is RollingUpdate." +# properties: +# maxUnavailable: +# description: "Maximum number of pods unavailable during a rolling update. Set to \"auto\" to auto-calculate as floor(replicaCount/2). Set to 0 to block all voluntary pod replacements. Must not exceed floor(replicaCount/2)." +# anyOf: +# - type: integer +# minimum: 0 +# - enum: [auto] +# if: +# properties: +# type: +# const: RollingUpdate +# then: +# required: +# - rollingUpdate +# properties: +# rollingUpdate: +# required: +# - maxUnavailable +# @schema +# -- StatefulSet update strategy configuration. +# When type is RollingUpdate, rollingUpdate.maxUnavailable is required. +# When type is OnDelete, rollingUpdate is optional and ignored. +updateStrategy: + # -- StatefulSet update strategy type. Use RollingUpdate (default) for zero-downtime + # upgrades or OnDelete for manual pod-by-pod control. + type: RollingUpdate + rollingUpdate: + # -- (string/int) Maximum number of pods that can be unavailable during a rolling update. + # Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. + # Set to 0 to block all voluntary pod replacements. Any positive value is used directly + # and must not exceed floor(replicaCount/2). Ignored when updateStrategy.type is OnDelete. + maxUnavailable: auto storage: # -- Storage class to use for Persistent Volume Claims (PVC) @@ -207,7 +263,7 @@ typesense: domains: [] cache: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 0 # @schema # -- LRU cache size for search responses. Unset uses Typesense default (1000) @@ -216,34 +272,34 @@ typesense: # -- Enable aggregated search query analytics enabled: false # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 1 # @schema # -- How often analytics are persisted to disk in seconds. Unset uses Typesense default (3600) flushInterval: snapshots: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 1 # @schema # -- Replication log snapshot frequency in seconds. Unset uses Typesense default (3600) intervalSeconds: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 1 # @schema # -- Concurrent request handler threads. Unset uses Typesense default (NUM_CORES * 8) threadPoolSize: limits: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 0 # maximum: 100 # @schema # -- Reject writes above this disk usage percentage. Unset uses Typesense default (100) diskUsedMaxPercentage: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 0 # maximum: 100 # @schema @@ -251,20 +307,20 @@ typesense: memoryUsedMaxPercentage: health: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 0 # @schema # -- Update lag threshold before rejecting reads. Unset uses Typesense default (1000) readLag: # @schema - # type: [integer, "null"] + # type: [integer, null] # minimum: 0 # @schema # -- Update lag threshold before rejecting writes. Unset uses Typesense default (500) writeLag: logging: # @schema - # type: [integer, "null"] + # type: [integer, null] # @schema # -- Threshold in ms for slow request logging (-1 disables). Unset uses Typesense default (-1) slowRequestsTimeMs: