From c7e8f4d975f349c2fb0c6ae6dab06c9d6daa1745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Thu, 2 Jul 2026 15:48:29 +0300 Subject: [PATCH 1/2] fix(mariadb): re-guard replication so single-replica passes the operator webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2279 made `replication.enabled: true` unconditional so the operator would always create -primary/-secondary services. But the mariadb-operator validating webhook (vmariadb-v1alpha1.kb.io) rejects replication when spec.replicas == 1, so single-replica MariaDB could no longer be created — the HelmRelease never became Ready. Re-guard replication on replicas>1 and teach the service-name consumers about the bare service the operator provisions in both topologies: - mariadb.yaml: gate `replication` on replicas>1; restore single-replica external access via `service.type: LoadBalancer` and keep `primaryService` LoadBalancer for replicas>1 only. - backup-cronjob.yaml: route single-replica dumps to the bare service (no -secondary exists without replication). - dashboard-resourcemap.yaml: always grant RBAC on the bare service, plus -primary/-secondary when replicas>1, matching the ApplicationDefinition. - mariadb-rd: list the bare service in services.include so single-replica installs are visible in the dashboard. Adds a single-replica e2e test that reproduces the webhook rejection and guards against regression. Assisted-By: Claude Signed-off-by: Алексей Артамонов --- hack/e2e-apps/mariadb.bats | 59 +++++++++++++++++++ .../mariadb/templates/backup-cronjob.yaml | 4 +- .../templates/dashboard-resourcemap.yaml | 6 ++ packages/apps/mariadb/templates/mariadb.yaml | 7 ++- .../system/mariadb-rd/cozyrds/mariadb.yaml | 1 + 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/hack/e2e-apps/mariadb.bats b/hack/e2e-apps/mariadb.bats index 0c1e3a5e8..74db318a9 100644 --- a/hack/e2e-apps/mariadb.bats +++ b/hack/e2e-apps/mariadb.bats @@ -49,3 +49,62 @@ EOF kubectl -n tenant-test wait deployment.apps/mariadb-$name-metrics --timeout=90s --for=jsonpath='{.status.replicas}'=1 kubectl -n tenant-test delete mariadbs.apps.cozystack.io $name } + +@test "Create single-replica MariaDB" { + name='single' + kubectl -n tenant-test delete mariadbs.apps.cozystack.io $name --ignore-not-found --timeout=2m + kubectl apply -f- </dev/null 2>&1; do sleep 2; done" + # A single-replica MariaDB must be accepted by the operator's validating + # webhook: replication is guarded on replicas>1, so replicas=1 renders a CR + # the webhook admits. Dump the HR + rendered CR on failure so a webhook + # rejection (the regression this guards against) is legible in the log. + kubectl -n tenant-test wait hr mariadb-$name --timeout=5m --for=condition=ready \ + || { echo "HR mariadb-$name not ready — dumping state:"; \ + kubectl -n tenant-test describe hr mariadb-$name; \ + kubectl -n tenant-test get mariadbs.k8s.mariadb.com $name -o yaml; false; } + # With replicas=1 the operator provisions the bare service (no + # -primary/-secondary): assert it exists and has an endpoint. + 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" + 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" + timeout 60 sh -ec "until kubectl -n tenant-test get statefulset.apps/mariadb-$name >/dev/null 2>&1; do sleep 2; done" + kubectl -n tenant-test wait statefulset.apps/mariadb-$name --timeout=110s --for=jsonpath='{.status.replicas}'=1 + 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" + 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" + timeout 60 sh -ec "until kubectl -n tenant-test get deployment.apps/mariadb-$name-metrics >/dev/null 2>&1; do sleep 2; done" + kubectl -n tenant-test wait deployment.apps/mariadb-$name-metrics --timeout=90s --for=jsonpath='{.status.replicas}'=1 + kubectl -n tenant-test delete mariadbs.apps.cozystack.io $name --ignore-not-found +} diff --git a/packages/apps/mariadb/templates/backup-cronjob.yaml b/packages/apps/mariadb/templates/backup-cronjob.yaml index ddb237cdd..9316e37a9 100644 --- a/packages/apps/mariadb/templates/backup-cronjob.yaml +++ b/packages/apps/mariadb/templates/backup-cronjob.yaml @@ -41,7 +41,9 @@ spec: name: {{ .Release.Name }} key: root-password - name: MYSQL_HOST - value: "{{ .Release.Name }}-{{ if eq (int .Values.replicas) 1 }}primary{{ else }}secondary{{ end }}" + # replicas>1 enables replication (offload dumps to a secondary); + # replicas=1 has no replication and only the bare service exists. + value: "{{ .Release.Name }}{{- if gt (int .Values.replicas) 1 }}-secondary{{- end }}" - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/packages/apps/mariadb/templates/dashboard-resourcemap.yaml b/packages/apps/mariadb/templates/dashboard-resourcemap.yaml index af89b39c0..a7c5731ea 100644 --- a/packages/apps/mariadb/templates/dashboard-resourcemap.yaml +++ b/packages/apps/mariadb/templates/dashboard-resourcemap.yaml @@ -8,8 +8,14 @@ rules: resources: - services resourceNames: + # The bare service exists in both topologies; -primary/-secondary + # only when replication is on (replicas>1). Grant exactly what exists so the + # RBAC matches the services the ApplicationDefinition (mariadb-rd) lists. + - {{ .Release.Name }} + {{- if gt (int .Values.replicas) 1 }} - {{ .Release.Name }}-primary - {{ .Release.Name }}-secondary + {{- end }} verbs: ["get", "list", "watch"] - apiGroups: - "" diff --git a/packages/apps/mariadb/templates/mariadb.yaml b/packages/apps/mariadb/templates/mariadb.yaml index 51bbdf74a..e6925591d 100644 --- a/packages/apps/mariadb/templates/mariadb.yaml +++ b/packages/apps/mariadb/templates/mariadb.yaml @@ -29,11 +29,13 @@ spec: - {{ .Release.Name }} topologyKey: "kubernetes.io/hostname" + {{- if gt (int .Values.replicas) 1 }} replication: enabled: true #primary: # podIndex: 0 # automaticFailover: true + {{- end }} podMetadata: labels: @@ -71,6 +73,9 @@ spec: metadata: labels: app.kubernetes.io/instance: {{ $.Release.Name }} + {{- if and .Values.external (eq (int .Values.replicas) 1) }} + type: LoadBalancer + {{- end }} storage: size: {{ .Values.size }} resizeInUseVolumes: true @@ -79,7 +84,7 @@ spec: storageClassName: {{ . }} {{- end }} - {{- if .Values.external }} + {{- if and .Values.external (gt (int .Values.replicas) 1) }} primaryService: type: LoadBalancer {{- end }} diff --git a/packages/system/mariadb-rd/cozyrds/mariadb.yaml b/packages/system/mariadb-rd/cozyrds/mariadb.yaml index ab1249b2f..122814052 100644 --- a/packages/system/mariadb-rd/cozyrds/mariadb.yaml +++ b/packages/system/mariadb-rd/cozyrds/mariadb.yaml @@ -35,5 +35,6 @@ spec: exclude: [] include: - resourceNames: + - mariadb-{{ .name }} - mariadb-{{ .name }}-primary - mariadb-{{ .name }}-secondary From f8e4222b7d7d42eac2ae2a39ef33da46c981b4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Thu, 2 Jul 2026 17:47:01 +0300 Subject: [PATCH 2/2] test(mariadb): fix operator MariaDB CR name in single-replica failure dump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The failure-path dump queried mariadbs.k8s.mariadb.com with $name, but the operator CR is named after the Helm release ({{ .Release.Name }} = mariadb-$name), so the dump would hit NotFound instead of showing the rejected CR. Assisted-By: Claude Signed-off-by: Алексей Артамонов --- hack/e2e-apps/mariadb.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/e2e-apps/mariadb.bats b/hack/e2e-apps/mariadb.bats index 74db318a9..e099018b1 100644 --- a/hack/e2e-apps/mariadb.bats +++ b/hack/e2e-apps/mariadb.bats @@ -95,7 +95,7 @@ EOF kubectl -n tenant-test wait hr mariadb-$name --timeout=5m --for=condition=ready \ || { echo "HR mariadb-$name not ready — dumping state:"; \ kubectl -n tenant-test describe hr mariadb-$name; \ - kubectl -n tenant-test get mariadbs.k8s.mariadb.com $name -o yaml; false; } + kubectl -n tenant-test get mariadbs.k8s.mariadb.com mariadb-$name -o yaml; false; } # With replicas=1 the operator provisions the bare service (no # -primary/-secondary): assert it exists and has an endpoint. 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"