Skip to content

Commit 71ca2b1

Browse files
authored
fix(mariadb): re-guard replication so single-replica passes the operator webhook (#3180)
## What this PR does Fixes a regression where **single-replica MariaDB can no longer be created**. [PR #2279](#2279) made `replication.enabled: true` unconditional in the MariaDB CR so the operator would always create `<release>-primary` / `<release>-secondary` Services (expected by the dashboard RBAC and the ApplicationDefinition). But the vendored mariadb-operator's validating webhook (`vmariadb-v1alpha1.kb.io`) **rejects `replication.enabled: true` when `spec.replicas == 1`** — replication needs a primary + at least one replica. As a result, single-replica installs never became Ready (the HelmRelease failed at admission). The webhook rule lives in the operator's Go code (not in the vendored CRD — there is no CEL rule tying replication to replica count), so the failing path is reproduced by an e2e test rather than a schema assertion. ### Fix — re-guard replication, teach consumers about the bare service The operator provisions a bare `<release>` Service in **both** topologies; `-primary`/`-secondary` exist **only** when replication is on (`replicas>1`). Instead of forcing replication for everyone, this PR lets replication track the replica count and makes the service-name consumers handle the single-replica bare service: - **`mariadb.yaml`** — gate `replication` on `replicas>1`; restore single-replica external access via `service.type: LoadBalancer`, and keep `primaryService: LoadBalancer` for `replicas>1` only. (Effectively reverts #2279's two hunks.) - **`backup-cronjob.yaml`** — single-replica dumps target the bare service (no `-secondary` exists without replication); `replicas>1` still offloads to `-secondary`. - **`dashboard-resourcemap.yaml`** — always grant RBAC on the bare service, plus `-primary`/`-secondary` when `replicas>1`, matching what the ApplicationDefinition lists. - **`mariadb-rd`** — list the bare service in `services.include` so single-replica installs are visible in the dashboard. - **`hack/e2e-apps/mariadb.bats`** — new `Create single-replica MariaDB` test that reproduces the webhook rejection and guards against regression. Verified: `helm template` at `replicas=1`/`2` renders the intended CR/RBAC/backup host; `helm unittest` passes; `update-crd.sh` keeps the RD stable; `select-e2e.sh` maps the change to the mariadb suite. ### Release note ```release-note fix(mariadb): single-replica instances can be created again — replication is now enabled only for replicas>1 (matching the operator webhook), while the dashboard, RBAC, and backups correctly target the bare service for single-replica setups ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an end-to-end test covering MariaDB deployments with a single replica, including validation of the main service, endpoints, StatefulSet replica count, and metrics components. * **Bug Fixes** * Updated MariaDB chart behavior to correctly handle single-replica vs multi-replica configurations for external access, replication settings, and backup host selection. * Refined dashboard RBAC and secret inclusion so access applies to the main MariaDB service in addition to replica-specific services. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 41fd08e + f8e4222 commit 71ca2b1

5 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎hack/e2e-apps/mariadb.bats‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,62 @@ EOF
4949
kubectl -n tenant-test wait deployment.apps/mariadb-$name-metrics --timeout=90s --for=jsonpath='{.status.replicas}'=1
5050
kubectl -n tenant-test delete mariadbs.apps.cozystack.io $name
5151
}
52+
53+
@test "Create single-replica MariaDB" {
54+
name='single'
55+
kubectl -n tenant-test delete mariadbs.apps.cozystack.io $name --ignore-not-found --timeout=2m
56+
kubectl apply -f- <<EOF
57+
apiVersion: apps.cozystack.io/v1alpha1
58+
kind: MariaDB
59+
metadata:
60+
name: $name
61+
namespace: tenant-test
62+
spec:
63+
external: false
64+
size: 10Gi
65+
replicas: 1
66+
storageClass: ""
67+
users:
68+
testuser:
69+
maxUserConnections: 1000
70+
password: xai7Wepo
71+
databases:
72+
testdb:
73+
roles:
74+
admin:
75+
- testuser
76+
backup:
77+
enabled: false
78+
s3Region: us-east-1
79+
s3Bucket: s3.example.org/mariadb-backups
80+
schedule: "0 2 * * *"
81+
cleanupStrategy: "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"
82+
s3AccessKey: oobaiRus9pah8PhohL1ThaeTa4UVa7gu
83+
s3SecretKey: ju3eum4dekeich9ahM1te8waeGai0oog
84+
resticPassword: ChaXoveekoh6eigh4siesheeda2quai0
85+
resources: {}
86+
resourcesPreset: "nano"
87+
EOF
88+
# Wait for the operator to materialise the HelmRelease before kubectl wait
89+
# kicks in (kubectl wait errors immediately if the object does not exist yet).
90+
timeout 60 sh -ec "until kubectl -n tenant-test get hr mariadb-$name >/dev/null 2>&1; do sleep 2; done"
91+
# A single-replica MariaDB must be accepted by the operator's validating
92+
# webhook: replication is guarded on replicas>1, so replicas=1 renders a CR
93+
# the webhook admits. Dump the HR + rendered CR on failure so a webhook
94+
# rejection (the regression this guards against) is legible in the log.
95+
kubectl -n tenant-test wait hr mariadb-$name --timeout=5m --for=condition=ready \
96+
|| { echo "HR mariadb-$name not ready — dumping state:"; \
97+
kubectl -n tenant-test describe hr mariadb-$name; \
98+
kubectl -n tenant-test get mariadbs.k8s.mariadb.com mariadb-$name -o yaml; false; }
99+
# With replicas=1 the operator provisions the bare <name> service (no
100+
# -primary/-secondary): assert it exists and has an endpoint.
101+
timeout 80 sh -ec "until kubectl -n tenant-test get svc mariadb-$name -o jsonpath='{.spec.ports[0].port}' | grep -q '3306'; do sleep 10; done"
102+
timeout 80 sh -ec "until kubectl -n tenant-test get endpoints mariadb-$name -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
103+
timeout 60 sh -ec "until kubectl -n tenant-test get statefulset.apps/mariadb-$name >/dev/null 2>&1; do sleep 2; done"
104+
kubectl -n tenant-test wait statefulset.apps/mariadb-$name --timeout=110s --for=jsonpath='{.status.replicas}'=1
105+
timeout 80 sh -ec "until kubectl -n tenant-test get svc mariadb-$name-metrics -o jsonpath='{.spec.ports[0].port}' | grep -q '9104'; do sleep 10; done"
106+
timeout 40 sh -ec "until kubectl -n tenant-test get endpoints mariadb-$name-metrics -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
107+
timeout 60 sh -ec "until kubectl -n tenant-test get deployment.apps/mariadb-$name-metrics >/dev/null 2>&1; do sleep 2; done"
108+
kubectl -n tenant-test wait deployment.apps/mariadb-$name-metrics --timeout=90s --for=jsonpath='{.status.replicas}'=1
109+
kubectl -n tenant-test delete mariadbs.apps.cozystack.io $name --ignore-not-found
110+
}

‎packages/apps/mariadb/templates/backup-cronjob.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ spec:
4141
name: {{ .Release.Name }}
4242
key: root-password
4343
- name: MYSQL_HOST
44-
value: "{{ .Release.Name }}-{{ if eq (int .Values.replicas) 1 }}primary{{ else }}secondary{{ end }}"
44+
# replicas>1 enables replication (offload dumps to a secondary);
45+
# replicas=1 has no replication and only the bare service exists.
46+
value: "{{ .Release.Name }}{{- if gt (int .Values.replicas) 1 }}-secondary{{- end }}"
4547
- name: AWS_ACCESS_KEY_ID
4648
valueFrom:
4749
secretKeyRef:

‎packages/apps/mariadb/templates/dashboard-resourcemap.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ rules:
88
resources:
99
- services
1010
resourceNames:
11+
# The bare <name> service exists in both topologies; -primary/-secondary
12+
# only when replication is on (replicas>1). Grant exactly what exists so the
13+
# RBAC matches the services the ApplicationDefinition (mariadb-rd) lists.
14+
- {{ .Release.Name }}
15+
{{- if gt (int .Values.replicas) 1 }}
1116
- {{ .Release.Name }}-primary
1217
- {{ .Release.Name }}-secondary
18+
{{- end }}
1319
verbs: ["get", "list", "watch"]
1420
- apiGroups:
1521
- ""

‎packages/apps/mariadb/templates/mariadb.yaml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ spec:
2929
- {{ .Release.Name }}
3030
topologyKey: "kubernetes.io/hostname"
3131

32+
{{- if gt (int .Values.replicas) 1 }}
3233
replication:
3334
enabled: true
3435
#primary:
3536
# podIndex: 0
3637
# automaticFailover: true
38+
{{- end }}
3739

3840
podMetadata:
3941
labels:
@@ -71,6 +73,9 @@ spec:
7173
metadata:
7274
labels:
7375
app.kubernetes.io/instance: {{ $.Release.Name }}
76+
{{- if and .Values.external (eq (int .Values.replicas) 1) }}
77+
type: LoadBalancer
78+
{{- end }}
7479
storage:
7580
size: {{ .Values.size }}
7681
resizeInUseVolumes: true
@@ -79,7 +84,7 @@ spec:
7984
storageClassName: {{ . }}
8085
{{- end }}
8186

82-
{{- if .Values.external }}
87+
{{- if and .Values.external (gt (int .Values.replicas) 1) }}
8388
primaryService:
8489
type: LoadBalancer
8590
{{- end }}

‎packages/system/mariadb-rd/cozyrds/mariadb.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ spec:
3535
exclude: []
3636
include:
3737
- resourceNames:
38+
- mariadb-{{ .name }}
3839
- mariadb-{{ .name }}-primary
3940
- mariadb-{{ .name }}-secondary

0 commit comments

Comments
 (0)