diff --git a/.github/workflows/helm-chart.yaml b/.github/workflows/helm-chart.yaml new file mode 100644 index 0000000000..f1bfe6e39e --- /dev/null +++ b/.github/workflows/helm-chart.yaml @@ -0,0 +1,59 @@ +################################################################################ +# 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. +################################################################################ + +name: "Helm Chart" + +permissions: + contents: read + +on: + pull_request: + branches: [main, release-*, ci-*] + paths: + - 'helm/**' + - '.github/workflows/helm-chart.yaml' + push: + branches: [main, release-*, ci-*] + paths: + - 'helm/**' + - '.github/workflows/helm-chart.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} + cancel-in-progress: true + +jobs: + test-helm-chart: + name: "Helm Lint and Unittest" + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Setup Helm" + uses: azure/setup-helm@v4 + + - name: "Lint Helm chart" + run: helm lint ./helm + + - name: "Run helm-unittest" + run: | + docker run --rm \ + -u "$(id -u):$(id -g)" \ + -v "${{ github.workspace }}":/apps \ + helmunittest/helm-unittest helm/ diff --git a/helm/README.md b/helm/README.md index f09750c489..837294e5d8 100644 --- a/helm/README.md +++ b/helm/README.md @@ -22,7 +22,7 @@ This chart deploys an Apache Fluss cluster on Kubernetes, following Helm best pr It requires a Zookeeper ensemble to be running in the same Kubernetes cluster. In future releases, we may add support for an embedded Zookeeper cluster. -## Development environment +## Development environment | component | version | | ------------------------------------------------------------------------------ | ------- | @@ -33,7 +33,7 @@ It requires a Zookeeper ensemble to be running in the same Kubernetes cluster. I | [Apache Fluss](https://fluss.apache.org/docs/) | v0.10.0-incubating | -## Image requirements +## Image requirements A container image for Fluss is available on DockerHub as `fluss/fluss`. You can use it directly or build your own from this repo. To use your own image you need to build the project with [Maven](https://fluss.apache.org/community/dev/building/) and build it with Docker. diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 79ae9d3106..8aa8196701 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -63,4 +63,54 @@ Selector labels {{- define "fluss.selectorLabels" -}} app.kubernetes.io/name: {{ include "fluss.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} \ No newline at end of file +{{- end }} + +{{/* +Generate JAAS configuration for SASL +*/}} +{{- define "fluss.sasl.jaasConfig" -}} +{{- if .Values.sasl.jaasConfig }} +{{- .Values.sasl.jaasConfig -}} +{{- else }} +FlussServer { + org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required + {{- range .Values.sasl.users }} + user_{{ .username }}="{{ .password }}" + {{- end }}; +}; +{{- end }} +{{- end }} + +{{/* +Return true if SASL is configured in any of the listener protocols +*/}} +{{- define "fluss.isSaslEnabled" -}} +{{- $ctx := . -}} +{{- $res := "" -}} +{{- $keys := keys .Values.listeners | sortAlpha -}} +{{- range $keys }} + {{- $id := . -}} + {{- $l := index $ctx.Values.listeners $id -}} + {{- if regexFind "SASL" (upper $l.protocol) -}} + {{- $res = "true" -}} + {{- end -}} +{{- end -}} +{{- if $res -}} +{{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Generate ID:SECURITY list for listener protocols +*/}} +{{- define "fluss.listeners.protocolMap" -}} +{{- $ctx := . -}} +{{- $parts := list -}} +{{- $keys := keys .Values.listeners | sortAlpha -}} +{{- range $keys }} + {{- $id := . -}} + {{- $l := index $ctx.Values.listeners $id -}} + {{- $parts = append $parts (printf "%s:%s" (upper $id) (upper $l.protocol)) -}} +{{- end -}} +{{- join "," $parts -}} +{{- end }} diff --git a/helm/templates/secret-sasl.yaml b/helm/templates/secret-sasl.yaml new file mode 100644 index 0000000000..9e413068cd --- /dev/null +++ b/helm/templates/secret-sasl.yaml @@ -0,0 +1,29 @@ +# +# 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. +# + +{{- if (include "fluss.isSaslEnabled" .) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "fluss.fullname" . }}-sasl-jaas-config + labels: + {{- include "fluss.labels" . | nindent 4 }} +type: Opaque +data: + jaas.conf: {{ include "fluss.sasl.jaasConfig" . | b64enc | quote }} +{{- end -}} diff --git a/helm/templates/sts-coordinator.yaml b/helm/templates/sts-coordinator.yaml index 6f2ec40ffc..b4bec267c3 100644 --- a/helm/templates/sts-coordinator.yaml +++ b/helm/templates/sts-coordinator.yaml @@ -77,6 +77,26 @@ spec: echo "" >> $FLUSS_HOME/conf/server.yaml && \ echo "bind.listeners: ${BIND_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ echo "advertised.listeners: ${ADVERTISED_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "security.protocol.map: {{ include "fluss.listeners.protocolMap" . }}" >> $FLUSS_HOME/conf/server.yaml && \ + + {{- if (include "fluss.isSaslEnabled" .) }} + {{- $saslUsers := default (list) .Values.sasl.users -}} + {{- if eq (len $saslUsers) 0 -}} + {{- fail "sasl.users must contain at least one user when SASL is enabled in listeners" -}} + {{- end }} + {{- $jaasUsers := list -}} + {{- range $saslUsers }} + {{- $jaasUsers = append $jaasUsers (printf "user_%s=\\\"%s\\\"" .username .password) -}} + {{- end }} + {{- $clientUser := first $saslUsers -}} + echo "security.sasl.enabled.mechanisms: {{ .Values.sasl.mechanism }}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "security.sasl.plain.jaas.config: org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required {{ join " " $jaasUsers }};" >> $FLUSS_HOME/conf/server.yaml && \ + + echo "client.security.protocol: SASL" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.mechanism: {{ .Values.sasl.mechanism }}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.username: {{ $clientUser.username }}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.password: {{ $clientUser.password }}" >> $FLUSS_HOME/conf/server.yaml && \ + {{- end }} bin/coordinator-server.sh start-foreground livenessProbe: @@ -100,6 +120,11 @@ spec: mountPath: /opt/conf - name: data mountPath: /tmp/fluss/data + {{- if (include "fluss.isSaslEnabled" .) }} + - name: sasl-config + mountPath: /etc/fluss/conf + readOnly: true + {{- end }} volumes: - name: fluss-conf configMap: @@ -108,6 +133,11 @@ spec: - name: data emptyDir: {} {{- end }} + {{- if (include "fluss.isSaslEnabled" .) }} + - name: sasl-config + secret: + secretName: {{ include "fluss.fullname" . }}-sasl-jaas-config + {{- end }} {{- if .Values.coordinator.storage.enabled }} volumeClaimTemplates: - metadata: diff --git a/helm/templates/sts-tablet.yaml b/helm/templates/sts-tablet.yaml index f329eb1f29..07e8dd5542 100644 --- a/helm/templates/sts-tablet.yaml +++ b/helm/templates/sts-tablet.yaml @@ -74,6 +74,26 @@ spec: echo "tablet-server.id: ${FLUSS_SERVER_ID}" >> $FLUSS_HOME/conf/server.yaml && \ echo "bind.listeners: ${BIND_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ echo "advertised.listeners: ${ADVERTISED_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "security.protocol.map: {{ include "fluss.listeners.protocolMap" . }}" >> $FLUSS_HOME/conf/server.yaml && \ + + {{- if (include "fluss.isSaslEnabled" .) }} + {{- $saslUsers := default (list) .Values.sasl.users -}} + {{- if eq (len $saslUsers) 0 -}} + {{- fail "sasl.users must contain at least one user when SASL is enabled in listeners" -}} + {{- end }} + {{- $jaasUsers := list -}} + {{- range $saslUsers }} + {{- $jaasUsers = append $jaasUsers (printf "user_%s=\\\"%s\\\"" .username .password) -}} + {{- end }} + {{- $clientUser := first $saslUsers -}} + echo "security.sasl.enabled.mechanisms: {{ .Values.sasl.mechanism }}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "security.sasl.plain.jaas.config: org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required {{ join " " $jaasUsers }};" >> $FLUSS_HOME/conf/server.yaml && \ + + echo "client.security.protocol: SASL" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.mechanism: {{ .Values.sasl.mechanism }}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.username: {{ $clientUser.username }}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.password: {{ $clientUser.password }}" >> $FLUSS_HOME/conf/server.yaml && \ + {{- end }} bin/tablet-server.sh start-foreground livenessProbe: @@ -97,6 +117,11 @@ spec: mountPath: /opt/conf - name: data mountPath: /tmp/fluss/data + {{- if (include "fluss.isSaslEnabled" .) }} + - name: sasl-config + mountPath: /etc/fluss/conf + readOnly: true + {{- end }} volumes: - name: fluss-conf configMap: @@ -105,6 +130,11 @@ spec: - name: data emptyDir: {} {{- end }} + {{- if (include "fluss.isSaslEnabled" .) }} + - name: sasl-config + secret: + secretName: {{ include "fluss.fullname" . }}-sasl-jaas-config + {{- end }} {{- if .Values.tablet.storage.enabled }} volumeClaimTemplates: - metadata: diff --git a/helm/tests/sasl_test.yaml b/helm/tests/sasl_test.yaml new file mode 100644 index 0000000000..44eea62b3d --- /dev/null +++ b/helm/tests/sasl_test.yaml @@ -0,0 +1,120 @@ +# +# 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: sasl-disabled-secret +templates: + - templates/secret-sasl.yaml +tests: + - it: does not render sasl secret when sasl is disabled + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + asserts: + - hasDocuments: + count: 0 +--- + +suite: sasl-disabled-statefulset +templates: + - templates/sts-coordinator.yaml +tests: + - it: does not render sasl settings in pod spec when sasl is disabled + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.enabled\.mechanisms:' + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.plain\.jaas\.config:' + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.sasl\.username:' + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.sasl\.password:' + - notContains: + path: spec.template.spec.volumes + content: + name: sasl-config +--- + +suite: sasl-enabled-generated-secret +templates: + - templates/secret-sasl.yaml +tests: + - it: renders generated sasl secret when sasl is enabled + set: + listeners.internal.protocol: SASL_PLAINTEXT + listeners.client.protocol: SASL_PLAINTEXT + asserts: + - hasDocuments: + count: 1 + - matchRegex: + path: metadata.name + pattern: ^RELEASE-NAME-fluss-sasl-jaas-config$ +--- + +suite: sasl-enabled-statefulset-uses-generated-secret +templates: + - templates/sts-coordinator.yaml +tests: + - it: references generated secret and renders sasl config lines + set: + listeners.internal.protocol: SASL_PLAINTEXT + listeners.client.protocol: SASL_PLAINTEXT + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: sasl-config + secret: + secretName: RELEASE-NAME-fluss-sasl-jaas-config + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.plain\.jaas\.config:' +--- + +suite: sasl-empty-users-fails +templates: + - templates/sts-coordinator.yaml +tests: + - it: fails with clear message when sasl users are empty + set: + listeners.internal.protocol: SASL_PLAINTEXT + listeners.client.protocol: SASL_PLAINTEXT + sasl.users: [] + asserts: + - failedTemplate: + errorMessage: sasl.users must contain at least one user when SASL is enabled in listeners +--- + +suite: server-yaml-write-path +templates: + - templates/sts-tablet.yaml +tests: + - it: writes server yaml using expected destination path + asserts: + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: cp /opt/conf/server\.yaml \$FLUSS_HOME/conf + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: '>> \$FLUSS_HOME/conf/server\.yaml' diff --git a/helm/values.yaml b/helm/values.yaml index db5d3fc2a9..661e3bc135 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -58,6 +58,15 @@ listeners: client: port: 9124 +# Fluss listener configurations +listeners: + internal: + protocol: PLAINTEXT + port: 9123 + client: + protocol: PLAINTEXT + port: 9124 + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little @@ -85,3 +94,12 @@ serviceAccount: # Additional annotations to apply to the ServiceAccount. # These can be useful, for example, to support integrations like workload identity. annotations: {} + +## Fluss SASL configurations for authentication. +## These are required if SASL listeners are configured. +sasl: + mechanism: PLAIN + # List of client users for authentication + users: + - username: admin + password: password diff --git a/website/docs/install-deploy/deploying-with-helm.md b/website/docs/install-deploy/deploying-with-helm.md index 9bdbf414e5..925fc4bb02 100644 --- a/website/docs/install-deploy/deploying-with-helm.md +++ b/website/docs/install-deploy/deploying-with-helm.md @@ -36,7 +36,7 @@ the installation documentation provides instructions for deploying one using Bit ### Running Fluss locally with Minikube -For local testing and development, you can deploy Fluss on Minikube. This is ideal for development, testing, and learning purposes. +For local testing and development, you can deploy Fluss on Minikube. This is ideal for development, testing and learning purposes. #### Prerequisites @@ -157,7 +157,7 @@ kubectl logs -l app.kubernetes.io/component=tablet ## Configuration Parameters -The following table lists the configurable parameters of the Fluss chart and their default values. +The following table lists the configurable parameters of the Fluss chart, and their default values. ### Global Parameters @@ -225,6 +225,12 @@ The following table lists the configurable parameters of the Fluss chart and the | `resources.tabletServer.limits.cpu` | CPU limits for tablet servers | Not set | | `resources.tabletServer.limits.memory` | Memory limits for tablet servers | Not set | +### SASL Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `sasl.mechanism` | SASL mechanism | `PLAIN` | +| `sasl.users` | User list for PLAIN authentication | `[{username: admin, password: password}]` | ## Advanced Configuration @@ -245,16 +251,47 @@ The chart automatically configures listeners for internal cluster communication - **Internal Port (9123)**: Used for internal communication within the cluster - **Client Port (9124)**: Used for client connections -Custom listener configuration: +Default listeners configuration: ```yaml listeners: internal: + protocol: PLAINTEXT port: 9123 client: + protocol: PLAINTEXT port: 9124 ``` +To enable SASL based authentication, set any of the protocols to `SASL`. + +### Enabling Secure Connection + +With the helm deployment, you can specify authentication protocols when connecting to the Fluss cluster. + +The following table shows the supported protocols and security they provide: + +| Method | Authentication | TLS Encryption | +|-------------|:--------------:|:------------------:| +| `PLAINTEXT` | No | No | +| `SASL` | Yes | No | + +By default, the `PLAINTEXT` protocol is used. + +The SASL authentication will be enabled if any of the listener protocols is using `SASL`. + +Set these values for additional configurations: + +```yaml +sasl: + mechanism: PLAIN + users: + - username: admin + password: password +``` + +The `users` field defines the list of usernames and passwords for SASL authentication. The first user in the list is used for internal client authentication in the chart templates. + ### Storage Configuration Configure different storage volumes for coordinator or tablet pods: