diff --git a/helm/templates/_security.tpl b/helm/templates/_security.tpl index b6a52d4060f..2b09c100c84 100644 --- a/helm/templates/_security.tpl +++ b/helm/templates/_security.tpl @@ -286,6 +286,49 @@ Usage: {{- include "fluss.security.sasl.warnInternalUser" . -}} {{- end -}} +{{/* +secretKeyRef env entry for the readiness probe credential, or empty when no +Secret is referenced. Rendered on every server container, so the exec probe +inherits it whichever component runs the cluster-health check. +Usage: + include "fluss.security.readinessProbe.env" . +*/}} +{{- define "fluss.security.readinessProbe.env" -}} +{{- $ref := .Values.security.readinessProbe.existingSecret | default (dict) -}} +{{- if $ref.name }} +- name: READINESS_HEALTH_CHECK_AUTH + valueFrom: + secretKeyRef: + name: {{ $ref.name }} + key: {{ $ref.key | default "auth" }} +{{- end }} +{{- end -}} + +{{/* +Validates the readiness probe credential. It has three possible sources, and +setting more than one is an error. +Returns an error message if invalid, empty string otherwise. +Usage: + include "fluss.security.readinessProbe.validateAuth" . +*/}} +{{- define "fluss.security.readinessProbe.validateAuth" -}} +{{- $sources := list -}} +{{- if .Values.tablet.readinessProbe.healthCheckAuth -}} + {{- $sources = append $sources "tablet.readinessProbe.healthCheckAuth" -}} +{{- end -}} +{{- if (.Values.security.readinessProbe.existingSecret | default (dict)).name -}} + {{- $sources = append $sources "security.readinessProbe.existingSecret" -}} +{{- end -}} +{{- range .Values.secrets.env -}} + {{- if eq .name "READINESS_HEALTH_CHECK_AUTH" -}} + {{- $sources = append $sources "the secrets.env entry named READINESS_HEALTH_CHECK_AUTH" -}} + {{- end -}} +{{- end -}} +{{- if gt (len $sources) 1 -}} + {{- printf "the readiness probe credential is set more than once (%s); set only one" (join ", " $sources) -}} +{{- end -}} +{{- end -}} + {{/* Collects security error messages. Usage: @@ -299,6 +342,7 @@ Usage: {{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validateLoginModuleClass" .) -}} {{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validateUsername" .) -}} {{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validatePassword" .) -}} +{{- $errMessages = append $errMessages (include "fluss.security.readinessProbe.validateAuth" .) -}} {{- $errMessages = without $errMessages "" -}} {{- join "\n" $errMessages -}} {{- end -}} diff --git a/helm/templates/sts-coordinator.yaml b/helm/templates/sts-coordinator.yaml index 583cfcd9cbd..c097f0be03b 100644 --- a/helm/templates/sts-coordinator.yaml +++ b/helm/templates/sts-coordinator.yaml @@ -105,6 +105,9 @@ spec: {{- if (include "fluss.secrets.enabled" .) }} {{- include "fluss.secrets.env" . | trim | nindent 12 }} {{- end }} + {{- with (include "fluss.security.readinessProbe.env" .) }} + {{- . | trim | nindent 12 }} + {{- end }} {{- with .Values.coordinator.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/helm/templates/sts-tablet.yaml b/helm/templates/sts-tablet.yaml index 5407b2a6a95..4f0526cfd10 100644 --- a/helm/templates/sts-tablet.yaml +++ b/helm/templates/sts-tablet.yaml @@ -101,6 +101,9 @@ spec: {{- if (include "fluss.secrets.enabled" .) }} {{- include "fluss.secrets.env" . | trim | nindent 12 }} {{- end }} + {{- with (include "fluss.security.readinessProbe.env" .) }} + {{- . | trim | nindent 12 }} + {{- end }} {{- with .Values.tablet.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/helm/tests/readiness_probe_auth_test.yaml b/helm/tests/readiness_probe_auth_test.yaml new file mode 100644 index 00000000000..b1b10aa1a9c --- /dev/null +++ b/helm/tests/readiness_probe_auth_test.yaml @@ -0,0 +1,114 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: readiness-probe-auth +templates: + - templates/sts-coordinator.yaml + - templates/sts-tablet.yaml + - templates/NOTES.txt +tests: + - it: renders no probe credential by default + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: READINESS_HEALTH_CHECK_AUTH + any: true + template: templates/sts-tablet.yaml + - notMatchRegex: + path: spec.template.spec.containers[0].readinessProbe.exec.command[2] + pattern: 'READINESS_HEALTH_CHECK_AUTH' + template: templates/sts-tablet.yaml + + - it: sources the probe credential from a Secret on every server container + set: + security.readinessProbe.existingSecret: + name: fluss-readiness-probe-auth + key: auth + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: READINESS_HEALTH_CHECK_AUTH + valueFrom: + secretKeyRef: + name: fluss-readiness-probe-auth + key: auth + template: templates/sts-tablet.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: READINESS_HEALTH_CHECK_AUTH + valueFrom: + secretKeyRef: + name: fluss-readiness-probe-auth + key: auth + template: templates/sts-coordinator.yaml + # The probe command must not export the variable, otherwise it would + # override the Secret-sourced value inherited from the container. + - notMatchRegex: + path: spec.template.spec.containers[0].readinessProbe.exec.command[2] + pattern: 'export READINESS_HEALTH_CHECK_AUTH' + template: templates/sts-tablet.yaml + + - it: defaults the Secret key to auth + set: + security.readinessProbe.existingSecret.name: fluss-readiness-probe-auth + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: READINESS_HEALTH_CHECK_AUTH + valueFrom: + secretKeyRef: + name: fluss-readiness-probe-auth + key: auth + template: templates/sts-tablet.yaml + + - it: fails when the credential is set both inline and via existingSecret + set: + tablet.readinessProbe.healthCheckAuth: "client.security.protocol:SASL" + security.readinessProbe.existingSecret.name: fluss-readiness-probe-auth + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nthe readiness probe credential is set more than once (tablet.readinessProbe.healthCheckAuth, security.readinessProbe.existingSecret); set only one" + template: templates/NOTES.txt + + - it: fails when the credential is set both via existingSecret and secrets.env + set: + security.readinessProbe.existingSecret.name: fluss-readiness-probe-auth + secrets.env: + - name: READINESS_HEALTH_CHECK_AUTH + secretName: fluss-readiness-probe-auth + key: auth + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nthe readiness probe credential is set more than once (security.readinessProbe.existingSecret, the secrets.env entry named READINESS_HEALTH_CHECK_AUTH); set only one" + template: templates/NOTES.txt + + - it: fails when the credential is set both inline and via secrets.env + set: + tablet.readinessProbe.healthCheckAuth: "client.security.protocol:SASL" + secrets.env: + - name: READINESS_HEALTH_CHECK_AUTH + secretName: fluss-readiness-probe-auth + key: auth + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nthe readiness probe credential is set more than once (tablet.readinessProbe.healthCheckAuth, the secrets.env entry named READINESS_HEALTH_CHECK_AUTH); set only one" + template: templates/NOTES.txt diff --git a/helm/values.yaml b/helm/values.yaml index de9c8eb42e0..e125a229f66 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -79,6 +79,13 @@ tablet: # or escaping needed). Required only when the local client listener # enforces SASL. Example: # healthCheckAuth: "client.security.protocol:SASL;client.sasl.mechanism:PLAIN;client.security.sasl.username:admin;client.security.sasl.password:admin-pass" + # + # This value is inlined into the StatefulSet's probe command, so it is + # readable by anyone who can read the rendered manifest. To keep the + # credential in a Secret instead, leave this empty and set + # `security.readinessProbe.existingSecret`. + # Setting both is rejected at render time, because the probe command + # would silently override the Secret. healthCheckAuth: "" extraVolumes: [] extraVolumeMounts: [] @@ -197,6 +204,19 @@ security: # usernameKey: username # passwordKey: password + # Credential for the cluster-health readiness probes, sourced from a Secret. + # The referenced key holds the same single-line string that + # `.readinessProbe.healthCheckAuth` takes as a literal; the chart + # renders it as the READINESS_HEALTH_CHECK_AUTH environment variable on every + # server container, so the exec probe picks it up unchanged and the credential + # never appears in the rendered manifest. It lives here rather than under a + # component because every component's probe authenticates with it. + readinessProbe: + existingSecret: {} + # existingSecret: + # name: fluss-readiness-probe-auth + # key: auth + zookeeper: sasl: # "" | plain diff --git a/website/docs/install-deploy/deploying-with-helm.md b/website/docs/install-deploy/deploying-with-helm.md index 4a483e14a74..1cb5c45c51b 100644 --- a/website/docs/install-deploy/deploying-with-helm.md +++ b/website/docs/install-deploy/deploying-with-helm.md @@ -193,6 +193,7 @@ The following table lists the configurable parameters of the Fluss chart, and th | `security.internal.sasl.plain.username` | Internal listener PLAIN username | `""` | | `security.internal.sasl.plain.password` | Internal listener PLAIN password | `""` | | `security.internal.sasl.plain.existingSecret` | Reference to a pre-existing Secret for internal SASL credentials | `{}` | +| `security.readinessProbe.existingSecret` | Reference to a pre-existing Secret holding the [readiness probe credential](#probe-credentials-on-a-sasl-cluster), `{name, key}` | `{}` | Only `plain` mechanism is supported for now. An empty string disables the SASL authentication, and maps to the `PLAINTEXT` protocol. @@ -873,6 +874,47 @@ tablet: | `failureThreshold` | `200` | Max consecutive probe failures before marking the pod as unready. With `periodSeconds=5`, this allows up to ~16 minutes for recovery. | | `periodSeconds` | `5` | How often the probe runs. | +##### Probe credentials on a SASL cluster + +The probe connects to the pod's own client listener as a regular Fluss client. When that +listener enforces SASL, the probe needs credentials or no TabletServer ever becomes Ready. + +The credential is a single-line string of semicolon-separated `key:value` pairs. Supply it in +one of two ways. + +Inline, which is simplest but puts the credential in plain text in the rendered StatefulSet: + +```yaml +tablet: + readinessProbe: + healthCheckAuth: "client.security.protocol:SASL;client.security.sasl.mechanism:PLAIN;client.security.sasl.username:probe;client.security.sasl.password:probe-pass" +``` + +Or from a Secret, which keeps it out of the manifest. Leave `healthCheckAuth` empty and +reference the Secret instead: + +```bash +kubectl create secret generic fluss-readiness-probe-auth \ + --from-literal=auth="client.security.protocol:SASL;client.security.sasl.mechanism:PLAIN;client.security.sasl.username:probe;client.security.sasl.password:probe-pass" +``` + +```yaml +security: + readinessProbe: + existingSecret: + name: fluss-readiness-probe-auth + key: auth # optional, defaults to `auth` +``` + +The chart renders it as the `READINESS_HEALTH_CHECK_AUTH` environment variable on every server +container, and the exec probe inherits the container environment, so the value reaches the +check unchanged. The setting sits under `security` rather than under a component because every +component's probe authenticates with the same credential. + +Setting the credential more than once fails the render. The inline form would silently +override the Secret, and a second `READINESS_HEALTH_CHECK_AUTH` entry under +[`secrets.env`](#secrets-in-configuration-overrides) would make the StatefulSet invalid. + :::note The CoordinatorServer does not need the Cluster Health API probe — it does not host data replicas, so a simple TCP check is sufficient. The Coordinator should be upgraded **after** all TabletServers are fully upgraded and recovered.