fix(redis): delete the Sentinel PDB left behind on scale-down

Setting sentinel.disablePodDisruptionBudget only stops the operator from
ensuring the PDB; it never deletes the one it created while three
Sentinels ran. An existing instance scaled to one Redis pod therefore
kept minAvailable 1 on its single Sentinel, which refuses every
eviction. A post-upgrade hook deletes rfs-<release> in that case.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
This commit is contained in:
Mattia Eleuteri committed 2026-11-02 11:23:46 +01:00
1 parent 48779cc03b
commit 7c1bd33b08
5 files changed
+206 -3

No files matched your search

+1 -1
View File
@@ -79,7 +79,7 @@ See [`docs/operations/resource-presets.md`](../../../docs/operations/resource-pr
### sentinel
`sentinel.resources` and `sentinel.resourcesPreset` size the three Sentinel pods separately from the Redis pods. A Sentinel holds only its configuration and the failover state, so it needs a small fraction of what a Redis replica holding the dataset does. When the `sentinel` block sets neither, the Sentinel pods are sized like the Redis pods, which is how every instance was sized before these parameters existed.
`sentinel.resources` and `sentinel.resourcesPreset` size the Sentinel pods separately from the Redis pods. A Sentinel holds only its configuration and the failover state, so it needs a small fraction of what a Redis replica holding the dataset does. When the `sentinel` block sets neither, the Sentinel pods are sized like the Redis pods, which is how every instance was sized before these parameters existed.
```yaml
resourcesPreset: u1.small
@@ -47,3 +47,7 @@ These presets are for basic testing and not meant to be used in production
{{- printf "ERROR: Preset key '%s' invalid. Allowed values are %s" .type (join "," (keys $presets)) | fail -}}
{{- end -}}
{{- end -}}
{{- define "redis.sentinelReplicas" -}}
{{- ternary 1 3 (le (int .Values.replicas) 1) -}}
{{- end -}}
@@ -0,0 +1,119 @@
{{- /*
An instance scaled down to one Redis pod keeps the Sentinel PDB the operator
created while it ran three Sentinels: setting disablePodDisruptionBudget only
stops the operator from ensuring it, never deletes it, and minAvailable 1 on
a single Sentinel refuses every eviction. A fresh install with one replica
never gets that PDB, so this runs on upgrade only.
*/}}
{{- if and .Release.IsUpgrade (eq (include "redis.sentinelReplicas" . | int) 1) }}
{{- $name := printf "%s-sentinel-pdb" .Release.Name }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ $name }}
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $name }}
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
rules:
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
resourceNames: ["rfs-{{ .Release.Name }}"]
verbs: ["get", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $name }}
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $name }}
subjects:
- kind: ServiceAccount
name: {{ $name }}
namespace: {{ .Release.Namespace }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ $name }}
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-weight: "5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
ttlSecondsAfterFinished: 3600
# Bounds the kubectl call too: a --request-timeout would make kubectl skip
# its in-cluster credentials (hack/check-hook-request-timeout.bats).
activeDeadlineSeconds: 120
backoffLimit: 2
template:
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
policy.cozystack.io/allow-to-apiserver: "true"
spec:
serviceAccountName: {{ $name }}
restartPolicy: Never
terminationGracePeriodSeconds: 10
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: delete-pdb
image: '{{ include "cozy-lib.image" (list "clastix/kubectl:v1.32@sha256:b9ef7d8dbe65bcc81a46c09b8dc7543103055021c4f43287bf59e92a8f4fe05c" $) }}'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
env:
- name: HOME
value: /tmp
command:
- kubectl
- --namespace={{ .Release.Namespace }}
- delete
- poddisruptionbudget
- rfs-{{ .Release.Name }}
- --ignore-not-found=true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
{{- end }}
@@ -14,9 +14,11 @@
watch it. One Sentinel still lets the operator find the master (its quorum is
replicas/2+1, so 1). The operator gives the Sentinel Deployment a PDB with
minAvailable 1 whenever Redis has two replicas or fewer, which on a single
Sentinel would block every node drain, so that PDB is dropped with it.
Sentinel would block every node drain, so that PDB is dropped with it; the
operator does not delete a PDB it stops managing, hence
hooks/sentinel-pdb.yaml.
*/}}
{{- $sentinelReplicas := ternary 1 3 (le (int .Values.replicas) 1) -}}
{{- $sentinelReplicas := include "redis.sentinelReplicas" . | int -}}
{{- $sentinelPreset := $sentinel.resourcesPreset | default .Values.resourcesPreset -}}
{{- $sentinelResources := .Values.resources -}}
{{- if or $sentinel.resources $sentinel.resourcesPreset -}}
@@ -0,0 +1,78 @@
suite: stale Sentinel PDB removed on scale-down to one Redis pod
# The operator only stops ensuring the Sentinel PDB once
# disablePodDisruptionBudget is set; it never deletes the one it created while
# three Sentinels ran. Left in place, its minAvailable 1 refuses every eviction
# of the single Sentinel and blocks node drains.
templates:
- templates/hooks/sentinel-pdb.yaml
release:
name: my-redis
namespace: tenant-test
set:
_cluster:
cluster-domain: cluster.local
_namespace:
host: example.com
tests:
- it: deletes the operator's Sentinel PDB on an upgrade to one replica
release:
upgrade: true
set:
replicas: 1
documentSelector:
path: kind
value: Job
asserts:
- equal:
path: metadata.annotations["helm.sh/hook"]
value: post-upgrade
- equal:
path: spec.template.spec.containers[0].command
value:
- kubectl
- --namespace=tenant-test
- delete
- poddisruptionbudget
- rfs-my-redis
- --ignore-not-found=true
- notMatchRegex:
path: spec.template.spec.containers[0].command[4]
pattern: request-timeout
- it: lets the hook delete only that PDB
release:
upgrade: true
set:
replicas: 1
documentSelector:
path: kind
value: Role
asserts:
- equal:
path: rules
value:
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
resourceNames: ["rfs-my-redis"]
verbs: ["get", "delete"]
- it: renders nothing on install, where no such PDB exists
set:
replicas: 1
asserts:
- hasDocuments:
count: 0
- it: renders nothing while three Sentinels run
release:
upgrade: true
set:
replicas: 2
asserts:
- hasDocuments:
count: 0