diff --git a/packages/apps/redis/README.md b/packages/apps/redis/README.md index 28152d9f9..478f75986 100644 --- a/packages/apps/redis/README.md +++ b/packages/apps/redis/README.md @@ -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 diff --git a/packages/apps/redis/templates/_resources.tpl b/packages/apps/redis/templates/_resources.tpl index 6539c99a1..98119dbb2 100644 --- a/packages/apps/redis/templates/_resources.tpl +++ b/packages/apps/redis/templates/_resources.tpl @@ -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 -}} diff --git a/packages/apps/redis/templates/hooks/sentinel-pdb.yaml b/packages/apps/redis/templates/hooks/sentinel-pdb.yaml new file mode 100644 index 000000000..e29613c8a --- /dev/null +++ b/packages/apps/redis/templates/hooks/sentinel-pdb.yaml @@ -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 }} diff --git a/packages/apps/redis/templates/redisfailover.yaml b/packages/apps/redis/templates/redisfailover.yaml index 2dd1fecaf..05a96e3cc 100644 --- a/packages/apps/redis/templates/redisfailover.yaml +++ b/packages/apps/redis/templates/redisfailover.yaml @@ -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 -}} diff --git a/packages/apps/redis/tests/sentinel_pdb_hook_test.yaml b/packages/apps/redis/tests/sentinel_pdb_hook_test.yaml new file mode 100644 index 000000000..71ef30e23 --- /dev/null +++ b/packages/apps/redis/tests/sentinel_pdb_hook_test.yaml @@ -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