diff --git a/Makefile b/Makefile index 93581a28..d44fe92d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ LOCAL_NODE_BIN := $(LOCAL_NODE_DIR)/bin LOCAL_NODE := $(LOCAL_NODE_BIN)/node LOCAL_NPM := $(LOCAL_NODE_BIN)/npm -.PHONY: proto build test test-nested-modules tidy lint generate build-sdk docker-build docker-build-e2e-client docker-build-etcd-tools docker-clean ensure-minio start-minio stop-containers release-broker-ports test-produce-consume test-produce-consume-debug test-consumer-group test-ops-api test-mcp test-multi-segment-durability test-full test-operator test-acl demo demo-platform demo-platform-bootstrap iceberg-demo kafsql-demo platform-demo help clean-kind-all ensure-local-node check vet race fmt fmt-check test-fuzz code-ql code-ql-summary code-ql-gate commit-check +.PHONY: proto build test test-nested-modules tidy lint generate build-sdk docker-build docker-build-e2e-client docker-build-etcd-tools docker-clean ensure-minio start-minio stop-containers release-broker-ports test-produce-consume test-produce-consume-debug test-consumer-group test-ops-api test-mcp test-multi-segment-durability test-full test-operator test-acl demo demo-platform demo-platform-bootstrap iceberg-demo kafsql-demo platform-demo help clean-kind-all ensure-local-node check vet race fmt fmt-check test-fuzz code-ql code-ql-summary code-ql-gate commit-check test-chart-proxy-nodeport REGISTRY ?= ghcr.io/kafscale STAMP_DIR ?= .build @@ -205,6 +205,9 @@ fmt-check: ## Check formatting (fails if unformatted) test-fuzz: ## Run Go fuzz test(s) bash scripts/test_fuzz.sh +test-chart-proxy-nodeport: ## Run the proxy Service nodePort chart template test (helm only, no cluster) + bash test/chart/proxy-nodeport_test.sh + code-ql: ensure-local-node ## Run local CodeQL and emit SARIF under .tmp/codeql/ bash scripts/codeql_local.sh diff --git a/deploy/helm/kafscale/README.md b/deploy/helm/kafscale/README.md index 4aa8cd22..e2706e44 100644 --- a/deploy/helm/kafscale/README.md +++ b/deploy/helm/kafscale/README.md @@ -111,6 +111,31 @@ See [values.yaml](values.yaml) for the full list of configurable parameters. | `lfsDemos.*` | Demo applications | | `mcp.*` | MCP server settings | +### Proxy Service + +The proxy is the single Kafka entrypoint, so its Service is the one clients connect to. + +| Key | Description | Default | +|-----|-------------|---------| +| `proxy.service.type` | Service type (`ClusterIP`, `NodePort`, `LoadBalancer`) | `LoadBalancer` | +| `proxy.service.port` | Service port for the Kafka listener | `9092` | +| `proxy.service.nodePort` | Pin the node port for the Kafka listener. Only rendered when `type == NodePort`; leave empty to let Kubernetes auto-assign | `""` | + +When `type` is `NodePort` and `nodePort` is empty, Kubernetes assigns a random +node port, so a fixed host-to-node-port mapping (for example a kind +`hostPort: 9092 -> containerPort: 30092` mapping) never reaches the proxy. Set +`proxy.service.nodePort` to a value in the valid NodePort range `30000-32767` +to pin it. A value outside that range renders fine but is rejected by the API +server at apply time. + +```yaml +proxy: + enabled: true + service: + type: NodePort + nodePort: 30092 +``` + ## LFS Proxy The LFS Proxy implements the claim-check pattern for large Kafka messages: diff --git a/deploy/helm/kafscale/templates/proxy-service.yaml b/deploy/helm/kafscale/templates/proxy-service.yaml index e884889f..dc4d0181 100644 --- a/deploy/helm/kafscale/templates/proxy-service.yaml +++ b/deploy/helm/kafscale/templates/proxy-service.yaml @@ -38,4 +38,7 @@ spec: port: {{ .Values.proxy.service.port }} targetPort: kafka protocol: TCP + {{- if and (eq .Values.proxy.service.type "NodePort") .Values.proxy.service.nodePort }} + nodePort: {{ .Values.proxy.service.nodePort }} + {{- end }} {{- end }} diff --git a/deploy/helm/kafscale/values.yaml b/deploy/helm/kafscale/values.yaml index a5ae4f9e..2b28eb61 100644 --- a/deploy/helm/kafscale/values.yaml +++ b/deploy/helm/kafscale/values.yaml @@ -151,6 +151,14 @@ proxy: port: 9092 annotations: {} loadBalancerSourceRanges: [] + # When type is NodePort, pin the node port (otherwise Kubernetes assigns a + # random one and a fixed host:port mapping cannot reach the proxy). Leave + # empty to let Kubernetes choose. Only rendered when type == NodePort. + # Valid range is 30000-32767; a value outside it renders but is rejected + # at apply time. Scoped to the proxy because it is the single Kafka + # entrypoint; console/mcp expose service.type but are left as-is and + # tracked separately. + nodePort: "" # NOTE: The standalone lfs-proxy has been removed. # LFS is now a feature-flag on the unified proxy: set proxy.lfs.enabled=true. diff --git a/test/chart/proxy-nodeport_test.sh b/test/chart/proxy-nodeport_test.sh new file mode 100755 index 00000000..07a8a7bc --- /dev/null +++ b/test/chart/proxy-nodeport_test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Copyright 2026 KafScale team. +# +# Licensed 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. + +# Chart template test for the proxy Service nodePort behaviour. +# +# Asserts that deploy/helm/kafscale/templates/proxy-service.yaml renders +# spec.ports[0].nodePort only when proxy.service.type == NodePort AND +# proxy.service.nodePort is set. Self-contained: needs only helm and awk, +# no helm plugins. Run directly or via `make test-chart-proxy-nodeport`. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHART_DIR="${ROOT_DIR}/deploy/helm/kafscale" + +command -v helm >/dev/null 2>&1 || { echo "helm is required"; exit 1; } + +fail=0 + +# Render the proxy Service object only. The chart guards the proxy template +# behind proxy.enabled, so every case enables it. +render_proxy_service() { + helm template kafscale "${CHART_DIR}" \ + --show-only templates/proxy-service.yaml \ + --set proxy.enabled=true \ + "$@" +} + +# nodePort value rendered on the kafka port, or empty string if absent. +proxy_nodeport() { + render_proxy_service "$@" | awk '/^[[:space:]]*nodePort:/ { print $2; exit }' +} + +assert_eq() { + local desc="$1" want="$2" got="$3" + if [ "${got}" = "${want}" ]; then + echo "PASS: ${desc} (nodePort=\"${got}\")" + else + echo "FAIL: ${desc} (want nodePort=\"${want}\", got \"${got}\")" + fail=1 + fi +} + +echo "==> chart proxy nodePort template test" + +# Case 1: NodePort + explicit nodePort -> renders nodePort: 30092. +got="$(proxy_nodeport --set proxy.service.type=NodePort --set proxy.service.nodePort=30092)" +assert_eq "NodePort + nodePort=30092 renders the value" "30092" "${got}" + +# Case 2: NodePort + empty nodePort -> line omitted (Kubernetes auto-assigns). +got="$(proxy_nodeport --set proxy.service.type=NodePort --set 'proxy.service.nodePort=')" +assert_eq "NodePort + empty nodePort omits the line" "" "${got}" + +# Case 3: LoadBalancer + value -> line suppressed (only NodePort honours it). +got="$(proxy_nodeport --set proxy.service.type=LoadBalancer --set proxy.service.nodePort=30092)" +assert_eq "LoadBalancer + nodePort=30092 suppresses the line" "" "${got}" + +if [ "${fail}" -ne 0 ]; then + echo "==> chart proxy nodePort template test FAILED" + exit 1 +fi +echo "==> chart proxy nodePort template test passed"