From 68679a02f4328e3cf9d1a81a300a01c2e888538a Mon Sep 17 00:00:00 2001 From: david-yu Date: Thu, 26 Mar 2026 13:31:53 -0700 Subject: [PATCH 1/6] feat: add per-listener LoadBalancer annotations for dedicated LB Services Add support for per-listener annotations and loadBalancerSourceRanges on ExternalListener. When an external listener has `annotations` set, it gets its own dedicated LoadBalancer Service per broker (named `lb--`) instead of sharing the default per-broker LB (`lb-`). This enables use cases like: - external-1: private LB with private DNS (internal annotations) - external-2: public LB with public DNS (internet-facing annotations) Listeners without per-listener annotations continue to share the default per-broker LB, preserving full backwards compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...dpanda-Added-20260326-per-listener-lb.yaml | 4 + .../templates/_service.loadbalancer.go.tpl | 192 ++++++++++++++++- .../redpanda/chart/templates/_values.go.tpl | 104 ++++++--- charts/redpanda/chart/values.schema.json | 59 +++++ charts/redpanda/service.loadbalancer.go | 201 +++++++++++++++--- charts/redpanda/values.go | 78 ++++++- charts/redpanda/values_partial.gen.go | 16 +- 7 files changed, 580 insertions(+), 74 deletions(-) create mode 100644 .changes/unreleased/charts-redpanda-Added-20260326-per-listener-lb.yaml diff --git a/.changes/unreleased/charts-redpanda-Added-20260326-per-listener-lb.yaml b/.changes/unreleased/charts-redpanda-Added-20260326-per-listener-lb.yaml new file mode 100644 index 000000000..b3bc2eaf2 --- /dev/null +++ b/.changes/unreleased/charts-redpanda-Added-20260326-per-listener-lb.yaml @@ -0,0 +1,4 @@ +project: charts/redpanda +kind: Added +body: Added per-listener LoadBalancer annotations support. When an external listener has `annotations` set, it gets a dedicated LoadBalancer Service per broker instead of sharing the default one. This enables use cases like private and public listeners on separate LBs with different cloud-provider annotations. +time: 2026-03-26T12:00:00.000000-04:00 diff --git a/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl b/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl index 4ed4e8a59..d913d4ece 100644 --- a/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl +++ b/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl @@ -1,6 +1,164 @@ {{- /* GENERATED FILE DO NOT EDIT */ -}} {{- /* Transpiled by gotohelm from "github.com/redpanda-data/redpanda-operator/charts/redpanda/v25/service.loadbalancer.go" */ -}} +{{- define "redpanda.dedicatedListenerNames" -}} +{{- $listeners := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $dedicated := (dict) -}} +{{- range $name, $l := $listeners.admin.external -}} +{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- $_ := (set $dedicated $name true) -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.kafka.external -}} +{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- $_ := (set $dedicated $name true) -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.http.external -}} +{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- $_ := (set $dedicated $name true) -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.schemaRegistry.external -}} +{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- $_ := (set $dedicated $name true) -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" $dedicated) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "redpanda.dedicatedListenerAnnotations" -}} +{{- $listeners := (index .a 0) -}} +{{- $listenerName := (index .a 1) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $merged := (dict) -}} +{{- range $name, $l := $listeners.admin.external -}} +{{- if (eq $name $listenerName) -}} +{{- range $k, $v := $l.annotations -}} +{{- $_ := (set $merged $k $v) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.kafka.external -}} +{{- if (eq $name $listenerName) -}} +{{- range $k, $v := $l.annotations -}} +{{- $_ := (set $merged $k $v) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.http.external -}} +{{- if (eq $name $listenerName) -}} +{{- range $k, $v := $l.annotations -}} +{{- $_ := (set $merged $k $v) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.schemaRegistry.external -}} +{{- if (eq $name $listenerName) -}} +{{- range $k, $v := $l.annotations -}} +{{- $_ := (set $merged $k $v) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" $merged) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "redpanda.dedicatedListenerSourceRanges" -}} +{{- $listeners := (index .a 0) -}} +{{- $listenerName := (index .a 1) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- range $name, $l := $listeners.kafka.external -}} +{{- if (and (eq $name $listenerName) (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.loadBalancerSourceRanges)))) "r") | int) (0 | int))) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.loadBalancerSourceRanges) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.admin.external -}} +{{- if (and (eq $name $listenerName) (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.loadBalancerSourceRanges)))) "r") | int) (0 | int))) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.loadBalancerSourceRanges) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.http.external -}} +{{- if (and (eq $name $listenerName) (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.loadBalancerSourceRanges)))) "r") | int) (0 | int))) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.loadBalancerSourceRanges) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.schemaRegistry.external -}} +{{- if (and (eq $name $listenerName) (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.loadBalancerSourceRanges)))) "r") | int) (0 | int))) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.loadBalancerSourceRanges) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" (coalesce nil)) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + {{- define "redpanda.LoadBalancerServices" -}} {{- $state := (index .a 0) -}} {{- range $_ := (list 1) -}} @@ -27,6 +185,7 @@ {{- if $_is_returning -}} {{- break -}} {{- end -}} +{{- $dedicated := (get (fromJson (include "redpanda.dedicatedListenerNames" (dict "a" (list $state.Values.listeners)))) "r") -}} {{- range $i, $podname := $pods -}} {{- $annotations := (dict) -}} {{- range $k, $v := $state.Values.external.annotations -}} @@ -56,13 +215,38 @@ {{- end -}} {{- $_ := (set $podSelector "statefulset.kubernetes.io/pod-name" $podname) -}} {{- $ports := (coalesce nil) -}} -{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePorts" (dict "a" (list $state.Values.listeners.admin "admin" $state.Values.external)))) "r"))) -}} -{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePorts" (dict "a" (list $state.Values.listeners.kafka "kafka" $state.Values.external)))) "r"))) -}} -{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePorts" (dict "a" (list $state.Values.listeners.http "http" $state.Values.external)))) "r"))) -}} -{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePorts" (dict "a" (list $state.Values.listeners.schemaRegistry "schema" $state.Values.external)))) "r"))) -}} +{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsExcludingListeners" (dict "a" (list $state.Values.listeners.admin "admin" $state.Values.external $dedicated)))) "r"))) -}} +{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsExcludingListeners" (dict "a" (list $state.Values.listeners.kafka "kafka" $state.Values.external $dedicated)))) "r"))) -}} +{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsExcludingListeners" (dict "a" (list $state.Values.listeners.http "http" $state.Values.external $dedicated)))) "r"))) -}} +{{- $ports = (concat (default (list) $ports) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsExcludingListeners" (dict "a" (list $state.Values.listeners.schemaRegistry "schema" $state.Values.external $dedicated)))) "r"))) -}} +{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $ports)))) "r") | int) (0 | int)) -}} {{- $svc := (mustMergeOverwrite (dict "metadata" (dict) "spec" (dict) "status" (dict "loadBalancer" (dict))) (mustMergeOverwrite (dict) (dict "apiVersion" "v1" "kind" "Service")) (dict "metadata" (mustMergeOverwrite (dict) (dict "name" (printf "lb-%s" $podname) "namespace" $state.Release.Namespace "labels" $labels "annotations" $annotations)) "spec" (mustMergeOverwrite (dict) (dict "externalTrafficPolicy" "Local" "loadBalancerSourceRanges" $state.Values.external.sourceRanges "ports" $ports "publishNotReadyAddresses" true "selector" $podSelector "sessionAffinity" "None" "type" "LoadBalancer")))) -}} {{- $services = (concat (default (list) $services) (list $svc)) -}} {{- end -}} +{{- range $listenerName, $_ := $dedicated -}} +{{- $dedicatedPorts := (coalesce nil) -}} +{{- $dedicatedPorts = (concat (default (list) $dedicatedPorts) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsForListener" (dict "a" (list $state.Values.listeners.admin "admin" $listenerName $state.Values.external)))) "r"))) -}} +{{- $dedicatedPorts = (concat (default (list) $dedicatedPorts) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsForListener" (dict "a" (list $state.Values.listeners.kafka "kafka" $listenerName $state.Values.external)))) "r"))) -}} +{{- $dedicatedPorts = (concat (default (list) $dedicatedPorts) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsForListener" (dict "a" (list $state.Values.listeners.http "http" $listenerName $state.Values.external)))) "r"))) -}} +{{- $dedicatedPorts = (concat (default (list) $dedicatedPorts) (default (list) (get (fromJson (include "redpanda.ListenerConfig.ServicePortsForListener" (dict "a" (list $state.Values.listeners.schemaRegistry "schema" $listenerName $state.Values.external)))) "r"))) -}} +{{- if (eq ((get (fromJson (include "_shims.len" (dict "a" (list $dedicatedPorts)))) "r") | int) (0 | int)) -}} +{{- continue -}} +{{- end -}} +{{- $dedicatedAnnotations := (dict) -}} +{{- range $k, $v := (get (fromJson (include "redpanda.dedicatedListenerAnnotations" (dict "a" (list $state.Values.listeners $listenerName)))) "r") -}} +{{- $_ := (set $dedicatedAnnotations $k $v) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $sourceRanges := (get (fromJson (include "redpanda.dedicatedListenerSourceRanges" (dict "a" (list $state.Values.listeners $listenerName)))) "r") -}} +{{- $svc := (mustMergeOverwrite (dict "metadata" (dict) "spec" (dict) "status" (dict "loadBalancer" (dict))) (mustMergeOverwrite (dict) (dict "apiVersion" "v1" "kind" "Service")) (dict "metadata" (mustMergeOverwrite (dict) (dict "name" (printf "lb-%s-%s" $listenerName $podname) "namespace" $state.Release.Namespace "labels" $labels "annotations" $dedicatedAnnotations)) "spec" (mustMergeOverwrite (dict) (dict "externalTrafficPolicy" "Local" "loadBalancerSourceRanges" $sourceRanges "ports" $dedicatedPorts "publishNotReadyAddresses" true "selector" $podSelector "sessionAffinity" "None" "type" "LoadBalancer")))) -}} +{{- $services = (concat (default (list) $services) (list $svc)) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- end -}} {{- if $_is_returning -}} {{- break -}} {{- end -}} diff --git a/charts/redpanda/chart/templates/_values.go.tpl b/charts/redpanda/chart/templates/_values.go.tpl index 818f2a1a6..c11332c4f 100644 --- a/charts/redpanda/chart/templates/_values.go.tpl +++ b/charts/redpanda/chart/templates/_values.go.tpl @@ -1192,6 +1192,60 @@ {{- end -}} {{- end -}} +{{- define "redpanda.ListenerConfig.ServicePortsForListener" -}} +{{- $l := (index .a 0) -}} +{{- $namePrefix := (index .a 1) -}} +{{- $listenerName := (index .a 2) -}} +{{- $external := (index .a 3) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $ports := (coalesce nil) -}} +{{- range $name, $listener := $l.external -}} +{{- if (ne $name $listenerName) -}} +{{- continue -}} +{{- end -}} +{{- if (not (get (fromJson (include "_shims.ptr_Deref" (dict "a" (list $listener.enabled $external.enabled)))) "r")) -}} +{{- continue -}} +{{- end -}} +{{- $fallbackPorts := (concat (default (list) $listener.advertisedPorts) (list ($l.port | int))) -}} +{{- $ports = (concat (default (list) $ports) (list (mustMergeOverwrite (dict "port" 0 "targetPort" 0) (dict "name" (printf "%s-%s" $namePrefix $name) "protocol" "TCP" "appProtocol" $l.appProtocol "targetPort" ($listener.port | int) "port" ((get (fromJson (include "_shims.ptr_Deref" (dict "a" (list $listener.nodePort (index $fallbackPorts (0 | int)))))) "r") | int))))) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" $ports) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "redpanda.ListenerConfig.ServicePortsExcludingListeners" -}} +{{- $l := (index .a 0) -}} +{{- $namePrefix := (index .a 1) -}} +{{- $external := (index .a 2) -}} +{{- $exclude := (index .a 3) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $ports := (coalesce nil) -}} +{{- range $name, $listener := $l.external -}} +{{- if (ternary (index $exclude $name) false (hasKey $exclude $name)) -}} +{{- continue -}} +{{- end -}} +{{- if (not (get (fromJson (include "_shims.ptr_Deref" (dict "a" (list $listener.enabled $external.enabled)))) "r")) -}} +{{- continue -}} +{{- end -}} +{{- $fallbackPorts := (concat (default (list) $listener.advertisedPorts) (list ($l.port | int))) -}} +{{- $ports = (concat (default (list) $ports) (list (mustMergeOverwrite (dict "port" 0 "targetPort" 0) (dict "name" (printf "%s-%s" $namePrefix $name) "protocol" "TCP" "appProtocol" $l.appProtocol "targetPort" ($listener.port | int) "port" ((get (fromJson (include "_shims.ptr_Deref" (dict "a" (list $listener.nodePort (index $fallbackPorts (0 | int)))))) "r") | int))))) -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" $ports) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + {{- define "redpanda.ListenerConfig.TrustStores" -}} {{- $l := (index .a 0) -}} {{- $tls := (index .a 1) -}} @@ -1286,7 +1340,7 @@ {{- $auth = $authAStr -}} {{- end -}} {{- $_is_returning = true -}} -{{- (dict "r" (mustMergeOverwrite (dict "enabled" (coalesce nil) "advertisedPorts" (coalesce nil) "port" 0 "nodePort" (coalesce nil) "tls" (coalesce nil)) (dict "enabled" $l.enabled "advertisedPorts" $l.advertisedPorts "port" ($l.port | int) "nodePort" $l.nodePort "tls" $l.tls "authenticationMethod" $auth "prefixTemplate" $l.prefixTemplate))) | toJson -}} +{{- (dict "r" (mustMergeOverwrite (dict "enabled" (coalesce nil) "advertisedPorts" (coalesce nil) "port" 0 "nodePort" (coalesce nil) "tls" (coalesce nil)) (dict "enabled" $l.enabled "advertisedPorts" $l.advertisedPorts "port" ($l.port | int) "nodePort" $l.nodePort "tls" $l.tls "authenticationMethod" $auth "prefixTemplate" $l.prefixTemplate "annotations" $l.annotations "loadBalancerSourceRanges" $l.loadBalancerSourceRanges))) | toJson -}} {{- break -}} {{- end -}} {{- end -}} @@ -1332,9 +1386,9 @@ {{- $result := (dict) -}} {{- range $k, $v := $c -}} {{- if (not (empty $v)) -}} -{{- $_1845___ok_15 := (get (fromJson (include "_shims.asnumeric" (dict "a" (list $v)))) "r") -}} -{{- $_ := ((index $_1845___ok_15 0) | float64) -}} -{{- $ok_15 := (index $_1845___ok_15 1) -}} +{{- $_1909___ok_15 := (get (fromJson (include "_shims.asnumeric" (dict "a" (list $v)))) "r") -}} +{{- $_ := ((index $_1909___ok_15 0) | float64) -}} +{{- $ok_15 := (index $_1909___ok_15 1) -}} {{- if $ok_15 -}} {{- $_ := (set $result $k $v) -}} {{- else -}}{{- if (kindIs "bool" $v) -}} @@ -1360,9 +1414,9 @@ {{- $_is_returning := false -}} {{- $result := (dict) -}} {{- range $k, $v := $c -}} -{{- $_1865_b_16_ok_17 := (get (fromJson (include "_shims.typetest" (dict "a" (list "bool" $v false)))) "r") -}} -{{- $b_16 := (index $_1865_b_16_ok_17 0) -}} -{{- $ok_17 := (index $_1865_b_16_ok_17 1) -}} +{{- $_1929_b_16_ok_17 := (get (fromJson (include "_shims.typetest" (dict "a" (list "bool" $v false)))) "r") -}} +{{- $b_16 := (index $_1929_b_16_ok_17 0) -}} +{{- $ok_17 := (index $_1929_b_16_ok_17 1) -}} {{- if $ok_17 -}} {{- $_ := (set $result $k $b_16) -}} {{- continue -}} @@ -1405,15 +1459,15 @@ {{- $config := (index .a 1) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $_1910___hasAccessKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_access_key" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1910___hasAccessKey 0) -}} -{{- $hasAccessKey := (index $_1910___hasAccessKey 1) -}} -{{- $_1911___hasSecretKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_secret_key" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1911___hasSecretKey 0) -}} -{{- $hasSecretKey := (index $_1911___hasSecretKey 1) -}} -{{- $_1912___hasSharedKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_azure_shared_key" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1912___hasSharedKey 0) -}} -{{- $hasSharedKey := (index $_1912___hasSharedKey 1) -}} +{{- $_1974___hasAccessKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_access_key" (coalesce nil))))) "r") -}} +{{- $_ := (index $_1974___hasAccessKey 0) -}} +{{- $hasAccessKey := (index $_1974___hasAccessKey 1) -}} +{{- $_1975___hasSecretKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_secret_key" (coalesce nil))))) "r") -}} +{{- $_ := (index $_1975___hasSecretKey 0) -}} +{{- $hasSecretKey := (index $_1975___hasSecretKey 1) -}} +{{- $_1976___hasSharedKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_azure_shared_key" (coalesce nil))))) "r") -}} +{{- $_ := (index $_1976___hasSharedKey 0) -}} +{{- $hasSharedKey := (index $_1976___hasSharedKey 1) -}} {{- $envvars := (coalesce nil) -}} {{- if (and (not $hasAccessKey) (get (fromJson (include "redpanda.SecretRef.IsValid" (dict "a" (list $tsc.accessKey)))) "r")) -}} {{- $envvars = (concat (default (list) $envvars) (list (mustMergeOverwrite (dict "name" "") (dict "name" "REDPANDA_CLOUD_STORAGE_ACCESS_KEY" "valueFrom" (get (fromJson (include "redpanda.SecretRef.AsSource" (dict "a" (list $tsc.accessKey)))) "r"))))) -}} @@ -1436,12 +1490,12 @@ {{- $c := (index .a 0) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $_1948___containerExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_container" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1948___containerExists 0) -}} -{{- $containerExists := (index $_1948___containerExists 1) -}} -{{- $_1949___accountExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_storage_account" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1949___accountExists 0) -}} -{{- $accountExists := (index $_1949___accountExists 1) -}} +{{- $_2012___containerExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_container" (coalesce nil))))) "r") -}} +{{- $_ := (index $_2012___containerExists 0) -}} +{{- $containerExists := (index $_2012___containerExists 1) -}} +{{- $_2013___accountExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_storage_account" (coalesce nil))))) "r") -}} +{{- $_ := (index $_2013___accountExists 0) -}} +{{- $accountExists := (index $_2013___accountExists 1) -}} {{- $_is_returning = true -}} {{- (dict "r" (and $containerExists $accountExists)) | toJson -}} {{- break -}} @@ -1452,9 +1506,9 @@ {{- $c := (index .a 0) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $_1954_value_ok := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c `cloud_storage_cache_size` (coalesce nil))))) "r") -}} -{{- $value := (index $_1954_value_ok 0) -}} -{{- $ok := (index $_1954_value_ok 1) -}} +{{- $_2018_value_ok := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c `cloud_storage_cache_size` (coalesce nil))))) "r") -}} +{{- $value := (index $_2018_value_ok 0) -}} +{{- $ok := (index $_2018_value_ok 1) -}} {{- if (not $ok) -}} {{- $_is_returning = true -}} {{- (dict "r" (coalesce nil)) | toJson -}} diff --git a/charts/redpanda/chart/values.schema.json b/charts/redpanda/chart/values.schema.json index 64e0b0ce1..382c76af0 100644 --- a/charts/redpanda/chart/values.schema.json +++ b/charts/redpanda/chart/values.schema.json @@ -3391,6 +3391,65 @@ "fullnameOverride": { "type": "string" }, + "gateway": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "hostnames": { + "oneOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "parentRefs": { + "oneOf": [ + { + "items": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "sectionName": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "path": { + "type": "string" + }, + "pathType": { + "type": "string" + } + }, + "type": "object" + }, "global": { "type": "object" }, diff --git a/charts/redpanda/service.loadbalancer.go b/charts/redpanda/service.loadbalancer.go index bf5fca8a8..adebe510e 100644 --- a/charts/redpanda/service.loadbalancer.go +++ b/charts/redpanda/service.loadbalancer.go @@ -20,6 +20,97 @@ import ( "github.com/redpanda-data/redpanda-operator/gotohelm/helmette" ) +// dedicatedListenerNames returns the set of external listener names that have +// per-listener annotations configured on any protocol. These listeners will get +// their own dedicated LoadBalancer Service per broker instead of sharing the +// default one. +func dedicatedListenerNames(listeners *Listeners) map[string]bool { + dedicated := map[string]bool{} + for name, l := range helmette.SortedMap(listeners.Admin.External) { + if len(l.Annotations) > 0 { + dedicated[name] = true + } + } + for name, l := range helmette.SortedMap(listeners.Kafka.External) { + if len(l.Annotations) > 0 { + dedicated[name] = true + } + } + for name, l := range helmette.SortedMap(listeners.HTTP.External) { + if len(l.Annotations) > 0 { + dedicated[name] = true + } + } + for name, l := range helmette.SortedMap(listeners.SchemaRegistry.External) { + if len(l.Annotations) > 0 { + dedicated[name] = true + } + } + return dedicated +} + +// dedicatedListenerAnnotations returns the merged annotations for a named +// listener across all protocols. If multiple protocols define annotations for +// the same listener name, they are merged (last write wins for duplicate keys). +func dedicatedListenerAnnotations(listeners *Listeners, listenerName string) map[string]string { + merged := map[string]string{} + for name, l := range helmette.SortedMap(listeners.Admin.External) { + if name == listenerName { + for k, v := range helmette.SortedMap(l.Annotations) { + merged[k] = v + } + } + } + for name, l := range helmette.SortedMap(listeners.Kafka.External) { + if name == listenerName { + for k, v := range helmette.SortedMap(l.Annotations) { + merged[k] = v + } + } + } + for name, l := range helmette.SortedMap(listeners.HTTP.External) { + if name == listenerName { + for k, v := range helmette.SortedMap(l.Annotations) { + merged[k] = v + } + } + } + for name, l := range helmette.SortedMap(listeners.SchemaRegistry.External) { + if name == listenerName { + for k, v := range helmette.SortedMap(l.Annotations) { + merged[k] = v + } + } + } + return merged +} + +// dedicatedListenerSourceRanges returns the LoadBalancerSourceRanges for a named +// listener. Uses the first non-empty value found across protocols. +func dedicatedListenerSourceRanges(listeners *Listeners, listenerName string) []string { + for name, l := range helmette.SortedMap(listeners.Kafka.External) { + if name == listenerName && len(l.LoadBalancerSourceRanges) > 0 { + return l.LoadBalancerSourceRanges + } + } + for name, l := range helmette.SortedMap(listeners.Admin.External) { + if name == listenerName && len(l.LoadBalancerSourceRanges) > 0 { + return l.LoadBalancerSourceRanges + } + } + for name, l := range helmette.SortedMap(listeners.HTTP.External) { + if name == listenerName && len(l.LoadBalancerSourceRanges) > 0 { + return l.LoadBalancerSourceRanges + } + } + for name, l := range helmette.SortedMap(listeners.SchemaRegistry.External) { + if name == listenerName && len(l.LoadBalancerSourceRanges) > 0 { + return l.LoadBalancerSourceRanges + } + } + return nil +} + func LoadBalancerServices(state *RenderState) []*corev1.Service { // This is technically a divergence from previous behavior but this matches // the NodePort's check and is more reasonable. @@ -47,6 +138,9 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { pods = append(pods, PodNames(state, set)...) } + // Identify listeners that should get their own dedicated LB Service. + dedicated := dedicatedListenerNames(&state.Values.Listeners) + for i, podname := range pods { // NB: A range loop is used here as its the most terse way to handle // nil maps in gotohelm. @@ -81,38 +175,83 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { podSelector["statefulset.kubernetes.io/pod-name"] = podname - // Divergences pop up here due to iterating over a map. This isn't okay - // in helm. TODO setup a linter that barks about this? Also a helper - // for getting the sorted keys of a map? + // Default shared LB: includes all listeners that do NOT have dedicated annotations. var ports []corev1.ServicePort - ports = append(ports, state.Values.Listeners.Admin.ServicePorts("admin", &state.Values.External)...) - ports = append(ports, state.Values.Listeners.Kafka.ServicePorts("kafka", &state.Values.External)...) - ports = append(ports, state.Values.Listeners.HTTP.ServicePorts("http", &state.Values.External)...) - ports = append(ports, state.Values.Listeners.SchemaRegistry.ServicePorts("schema", &state.Values.External)...) - - svc := &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Service", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("lb-%s", podname), - Namespace: state.Release.Namespace, - Labels: labels, - Annotations: annotations, - }, - Spec: corev1.ServiceSpec{ - ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, - LoadBalancerSourceRanges: state.Values.External.SourceRanges, - Ports: ports, - PublishNotReadyAddresses: true, - Selector: podSelector, - SessionAffinity: corev1.ServiceAffinityNone, - Type: corev1.ServiceTypeLoadBalancer, - }, - } - - services = append(services, svc) + ports = append(ports, state.Values.Listeners.Admin.ServicePortsExcludingListeners("admin", &state.Values.External, dedicated)...) + ports = append(ports, state.Values.Listeners.Kafka.ServicePortsExcludingListeners("kafka", &state.Values.External, dedicated)...) + ports = append(ports, state.Values.Listeners.HTTP.ServicePortsExcludingListeners("http", &state.Values.External, dedicated)...) + ports = append(ports, state.Values.Listeners.SchemaRegistry.ServicePortsExcludingListeners("schema", &state.Values.External, dedicated)...) + + // Only create the shared LB if it has ports remaining. + if len(ports) > 0 { + svc := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("lb-%s", podname), + Namespace: state.Release.Namespace, + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.ServiceSpec{ + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, + LoadBalancerSourceRanges: state.Values.External.SourceRanges, + Ports: ports, + PublishNotReadyAddresses: true, + Selector: podSelector, + SessionAffinity: corev1.ServiceAffinityNone, + Type: corev1.ServiceTypeLoadBalancer, + }, + } + + services = append(services, svc) + } + + // Dedicated LBs: one per listener name that has annotations. + for listenerName := range helmette.SortedMap(dedicated) { + var dedicatedPorts []corev1.ServicePort + dedicatedPorts = append(dedicatedPorts, state.Values.Listeners.Admin.ServicePortsForListener("admin", listenerName, &state.Values.External)...) + dedicatedPorts = append(dedicatedPorts, state.Values.Listeners.Kafka.ServicePortsForListener("kafka", listenerName, &state.Values.External)...) + dedicatedPorts = append(dedicatedPorts, state.Values.Listeners.HTTP.ServicePortsForListener("http", listenerName, &state.Values.External)...) + dedicatedPorts = append(dedicatedPorts, state.Values.Listeners.SchemaRegistry.ServicePortsForListener("schema", listenerName, &state.Values.External)...) + + if len(dedicatedPorts) == 0 { + continue + } + + dedicatedAnnotations := map[string]string{} + for k, v := range helmette.SortedMap(dedicatedListenerAnnotations(&state.Values.Listeners, listenerName)) { + dedicatedAnnotations[k] = v + } + + sourceRanges := dedicatedListenerSourceRanges(&state.Values.Listeners, listenerName) + + svc := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("lb-%s-%s", listenerName, podname), + Namespace: state.Release.Namespace, + Labels: labels, + Annotations: dedicatedAnnotations, + }, + Spec: corev1.ServiceSpec{ + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, + LoadBalancerSourceRanges: sourceRanges, + Ports: dedicatedPorts, + PublishNotReadyAddresses: true, + Selector: podSelector, + SessionAffinity: corev1.ServiceAffinityNone, + Type: corev1.ServiceTypeLoadBalancer, + }, + } + + services = append(services, svc) + } } return services diff --git a/charts/redpanda/values.go b/charts/redpanda/values.go index 56346efc7..3c26cbaf5 100644 --- a/charts/redpanda/values.go +++ b/charts/redpanda/values.go @@ -1674,6 +1674,56 @@ func (l *ListenerConfig[T]) ServicePorts(namePrefix string, external *ExternalCo return ports } +// ServicePortsForListener returns the ServicePort for a single named external +// listener, if it is enabled. +func (l *ListenerConfig[T]) ServicePortsForListener(namePrefix string, listenerName string, external *ExternalConfig) []corev1.ServicePort { + var ports []corev1.ServicePort + for name, listener := range helmette.SortedMap(l.External) { + if name != listenerName { + continue + } + if !ptr.Deref(listener.Enabled, external.Enabled) { + continue + } + + fallbackPorts := append(listener.AdvertisedPorts, l.Port) + + ports = append(ports, corev1.ServicePort{ + Name: fmt.Sprintf("%s-%s", namePrefix, name), + Protocol: corev1.ProtocolTCP, + AppProtocol: l.AppProtocol, + TargetPort: intstr.FromInt32(listener.Port), + Port: ptr.Deref(listener.NodePort, fallbackPorts[0]), + }) + } + return ports +} + +// ServicePortsExcludingListeners returns the ServicePorts for all enabled +// external listeners except those in the exclude set. +func (l *ListenerConfig[T]) ServicePortsExcludingListeners(namePrefix string, external *ExternalConfig, exclude map[string]bool) []corev1.ServicePort { + var ports []corev1.ServicePort + for name, listener := range helmette.SortedMap(l.External) { + if exclude[name] { + continue + } + if !ptr.Deref(listener.Enabled, external.Enabled) { + continue + } + + fallbackPorts := append(listener.AdvertisedPorts, l.Port) + + ports = append(ports, corev1.ServicePort{ + Name: fmt.Sprintf("%s-%s", namePrefix, name), + Protocol: corev1.ProtocolTCP, + AppProtocol: l.AppProtocol, + TargetPort: intstr.FromInt32(listener.Port), + Port: ptr.Deref(listener.NodePort, fallbackPorts[0]), + }) + } + return ports +} + // TrustStores returns a slice of all configured and enabled [TrustStore]s on // both internal and external listeners. func (l *ListenerConfig[T]) TrustStores(tls *TLS) []*TrustStore { @@ -1773,6 +1823,18 @@ type ExternalListener[T ~string] struct { AuthenticationMethod *T `json:"authenticationMethod,omitempty"` PrefixTemplate *string `json:"prefixTemplate,omitempty"` + + // Annotations, when set, causes this listener to be served by a dedicated + // per-broker LoadBalancer Service with these annotations, instead of sharing + // the default per-broker LoadBalancer. This enables use cases like having a + // private listener on one LB and a public listener on another, each with + // different cloud-provider annotations. + Annotations map[string]string `json:"annotations,omitempty"` + + // LoadBalancerSourceRanges, when set, restricts traffic to the dedicated + // LoadBalancer for this listener to the specified CIDRs. Only takes effect + // when Annotations is also set (i.e., when this listener has a dedicated LB). + LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"` } func (l *ExternalListener[T]) AsString() ExternalListener[string] { @@ -1783,13 +1845,15 @@ func (l *ExternalListener[T]) AsString() ExternalListener[string] { } return ExternalListener[string]{ - Enabled: l.Enabled, - AdvertisedPorts: l.AdvertisedPorts, - Port: l.Port, - NodePort: l.NodePort, - TLS: l.TLS, - AuthenticationMethod: auth, - PrefixTemplate: l.PrefixTemplate, + Enabled: l.Enabled, + AdvertisedPorts: l.AdvertisedPorts, + Port: l.Port, + NodePort: l.NodePort, + TLS: l.TLS, + AuthenticationMethod: auth, + PrefixTemplate: l.PrefixTemplate, + Annotations: l.Annotations, + LoadBalancerSourceRanges: l.LoadBalancerSourceRanges, } } diff --git a/charts/redpanda/values_partial.gen.go b/charts/redpanda/values_partial.gen.go index 626ad73c2..09112dc60 100644 --- a/charts/redpanda/values_partial.gen.go +++ b/charts/redpanda/values_partial.gen.go @@ -404,13 +404,15 @@ type PartialSASLUser struct { } type PartialExternalListener[T ~string] struct { - Enabled *bool "json:\"enabled,omitempty\"" - AdvertisedPorts []int32 "json:\"advertisedPorts,omitempty\" jsonschema:\"minItems=1\"" - Port *int32 "json:\"port,omitempty\" jsonschema:\"required\"" - NodePort *int32 "json:\"nodePort,omitempty\"" - TLS *PartialExternalTLS "json:\"tls,omitempty\"" - AuthenticationMethod *T "json:\"authenticationMethod,omitempty\"" - PrefixTemplate *string "json:\"prefixTemplate,omitempty\"" + Enabled *bool "json:\"enabled,omitempty\"" + AdvertisedPorts []int32 "json:\"advertisedPorts,omitempty\" jsonschema:\"minItems=1\"" + Port *int32 "json:\"port,omitempty\" jsonschema:\"required\"" + NodePort *int32 "json:\"nodePort,omitempty\"" + TLS *PartialExternalTLS "json:\"tls,omitempty\"" + AuthenticationMethod *T "json:\"authenticationMethod,omitempty\"" + PrefixTemplate *string "json:\"prefixTemplate,omitempty\"" + Annotations map[string]string "json:\"annotations,omitempty\"" + LoadBalancerSourceRanges []string "json:\"loadBalancerSourceRanges,omitempty\"" } type PartialTrustStore struct { From 8df84474dd22d6a26ec188d83684504edc0e43aa Mon Sep 17 00:00:00 2001 From: david-yu Date: Thu, 26 Mar 2026 13:45:48 -0700 Subject: [PATCH 2/6] fix: add annotations and loadBalancerSourceRanges to CRD ExternalListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TestHelmValuesCompat test requires the CRD ExternalListener type to have the same fields as the Helm chart ExternalListener so that CRD→Helm serialization round-trips correctly. Also regenerated the values schema. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/redpanda/v1alpha2/redpanda_clusterspec_types.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go b/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go index 150338b8a..30a26d3fa 100644 --- a/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go +++ b/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go @@ -973,6 +973,15 @@ type ExternalListener struct { // Specifies the network port that the external Service listens on. AdvertisedPorts []int32 `json:"advertisedPorts,omitempty"` NodePort *int32 `json:"nodePort,omitempty"` + + // Annotations, when set, causes this listener to be served by a dedicated + // per-broker LoadBalancer Service with these annotations, instead of sharing + // the default per-broker LoadBalancer. + Annotations map[string]string `json:"annotations,omitempty"` + + // LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + // for this listener. Only takes effect when Annotations is also set. + LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"` } // Admin configures settings for the Admin API listeners. From cca0e7e07a699b38599d4dfb92bdd3b592393de4 Mon Sep 17 00:00:00 2001 From: david-yu Date: Thu, 26 Mar 2026 14:09:52 -0700 Subject: [PATCH 3/6] refactor: use per-listener Type field instead of annotations as trigger Replace the implicit annotations-trigger-split model with an explicit Type field on ExternalListener. When Type is set on a listener, it gets its own dedicated per-broker Service of that type. When not set, it shares the default per-broker Service (existing behavior). This aligns more closely with Strimzi's design where each listener is an independent access path with its own service type, while maintaining full backwards compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../templates/_service.loadbalancer.go.tpl | 62 +++++++++++++++++-- .../redpanda/chart/templates/_values.go.tpl | 60 ++++++++++-------- charts/redpanda/service.loadbalancer.go | 49 +++++++++++---- charts/redpanda/values.go | 28 ++++++--- charts/redpanda/values_partial.gen.go | 1 + .../v1alpha2/redpanda_clusterspec_types.go | 12 ++-- 6 files changed, 159 insertions(+), 53 deletions(-) diff --git a/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl b/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl index d913d4ece..8627d8232 100644 --- a/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl +++ b/charts/redpanda/chart/templates/_service.loadbalancer.go.tpl @@ -7,7 +7,7 @@ {{- $_is_returning := false -}} {{- $dedicated := (dict) -}} {{- range $name, $l := $listeners.admin.external -}} -{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- if (get (fromJson (include "redpanda.ExternalListener.HasDedicatedService" (dict "a" (list $l)))) "r") -}} {{- $_ := (set $dedicated $name true) -}} {{- end -}} {{- end -}} @@ -15,7 +15,7 @@ {{- break -}} {{- end -}} {{- range $name, $l := $listeners.kafka.external -}} -{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- if (get (fromJson (include "redpanda.ExternalListener.HasDedicatedService" (dict "a" (list $l)))) "r") -}} {{- $_ := (set $dedicated $name true) -}} {{- end -}} {{- end -}} @@ -23,7 +23,7 @@ {{- break -}} {{- end -}} {{- range $name, $l := $listeners.http.external -}} -{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- if (get (fromJson (include "redpanda.ExternalListener.HasDedicatedService" (dict "a" (list $l)))) "r") -}} {{- $_ := (set $dedicated $name true) -}} {{- end -}} {{- end -}} @@ -31,7 +31,7 @@ {{- break -}} {{- end -}} {{- range $name, $l := $listeners.schemaRegistry.external -}} -{{- if (gt ((get (fromJson (include "_shims.len" (dict "a" (list $l.annotations)))) "r") | int) (0 | int)) -}} +{{- if (get (fromJson (include "redpanda.ExternalListener.HasDedicatedService" (dict "a" (list $l)))) "r") -}} {{- $_ := (set $dedicated $name true) -}} {{- end -}} {{- end -}} @@ -159,6 +159,57 @@ {{- end -}} {{- end -}} +{{- define "redpanda.dedicatedListenerServiceType" -}} +{{- $listeners := (index .a 0) -}} +{{- $listenerName := (index .a 1) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- range $name, $l := $listeners.kafka.external -}} +{{- if (and (eq $name $listenerName) (ne (toJson $l.type) "null")) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.type) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.admin.external -}} +{{- if (and (eq $name $listenerName) (ne (toJson $l.type) "null")) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.type) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.http.external -}} +{{- if (and (eq $name $listenerName) (ne (toJson $l.type) "null")) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.type) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- range $name, $l := $listeners.schemaRegistry.external -}} +{{- if (and (eq $name $listenerName) (ne (toJson $l.type) "null")) -}} +{{- $_is_returning = true -}} +{{- (dict "r" $l.type) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- if $_is_returning -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" "LoadBalancer") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + {{- define "redpanda.LoadBalancerServices" -}} {{- $state := (index .a 0) -}} {{- range $_ := (list 1) -}} @@ -239,8 +290,9 @@ {{- if $_is_returning -}} {{- break -}} {{- end -}} +{{- $svcType := (get (fromJson (include "redpanda.dedicatedListenerServiceType" (dict "a" (list $state.Values.listeners $listenerName)))) "r") -}} {{- $sourceRanges := (get (fromJson (include "redpanda.dedicatedListenerSourceRanges" (dict "a" (list $state.Values.listeners $listenerName)))) "r") -}} -{{- $svc := (mustMergeOverwrite (dict "metadata" (dict) "spec" (dict) "status" (dict "loadBalancer" (dict))) (mustMergeOverwrite (dict) (dict "apiVersion" "v1" "kind" "Service")) (dict "metadata" (mustMergeOverwrite (dict) (dict "name" (printf "lb-%s-%s" $listenerName $podname) "namespace" $state.Release.Namespace "labels" $labels "annotations" $dedicatedAnnotations)) "spec" (mustMergeOverwrite (dict) (dict "externalTrafficPolicy" "Local" "loadBalancerSourceRanges" $sourceRanges "ports" $dedicatedPorts "publishNotReadyAddresses" true "selector" $podSelector "sessionAffinity" "None" "type" "LoadBalancer")))) -}} +{{- $svc := (mustMergeOverwrite (dict "metadata" (dict) "spec" (dict) "status" (dict "loadBalancer" (dict))) (mustMergeOverwrite (dict) (dict "apiVersion" "v1" "kind" "Service")) (dict "metadata" (mustMergeOverwrite (dict) (dict "name" (printf "lb-%s-%s" $listenerName $podname) "namespace" $state.Release.Namespace "labels" $labels "annotations" $dedicatedAnnotations)) "spec" (mustMergeOverwrite (dict) (dict "externalTrafficPolicy" "Local" "loadBalancerSourceRanges" $sourceRanges "ports" $dedicatedPorts "publishNotReadyAddresses" true "selector" $podSelector "sessionAffinity" "None" "type" $svcType)))) -}} {{- $services = (concat (default (list) $services) (list $svc)) -}} {{- end -}} {{- if $_is_returning -}} diff --git a/charts/redpanda/chart/templates/_values.go.tpl b/charts/redpanda/chart/templates/_values.go.tpl index c11332c4f..bdbf68368 100644 --- a/charts/redpanda/chart/templates/_values.go.tpl +++ b/charts/redpanda/chart/templates/_values.go.tpl @@ -1330,6 +1330,16 @@ {{- end -}} {{- end -}} +{{- define "redpanda.ExternalListener.HasDedicatedService" -}} +{{- $l := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $_is_returning = true -}} +{{- (dict "r" (ne (toJson $l.type) "null")) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + {{- define "redpanda.ExternalListener.AsString" -}} {{- $l := (index .a 0) -}} {{- range $_ := (list 1) -}} @@ -1340,7 +1350,7 @@ {{- $auth = $authAStr -}} {{- end -}} {{- $_is_returning = true -}} -{{- (dict "r" (mustMergeOverwrite (dict "enabled" (coalesce nil) "advertisedPorts" (coalesce nil) "port" 0 "nodePort" (coalesce nil) "tls" (coalesce nil)) (dict "enabled" $l.enabled "advertisedPorts" $l.advertisedPorts "port" ($l.port | int) "nodePort" $l.nodePort "tls" $l.tls "authenticationMethod" $auth "prefixTemplate" $l.prefixTemplate "annotations" $l.annotations "loadBalancerSourceRanges" $l.loadBalancerSourceRanges))) | toJson -}} +{{- (dict "r" (mustMergeOverwrite (dict "enabled" (coalesce nil) "advertisedPorts" (coalesce nil) "port" 0 "nodePort" (coalesce nil) "tls" (coalesce nil)) (dict "enabled" $l.enabled "advertisedPorts" $l.advertisedPorts "port" ($l.port | int) "nodePort" $l.nodePort "tls" $l.tls "authenticationMethod" $auth "prefixTemplate" $l.prefixTemplate "type" $l.type "annotations" $l.annotations "loadBalancerSourceRanges" $l.loadBalancerSourceRanges))) | toJson -}} {{- break -}} {{- end -}} {{- end -}} @@ -1386,9 +1396,9 @@ {{- $result := (dict) -}} {{- range $k, $v := $c -}} {{- if (not (empty $v)) -}} -{{- $_1909___ok_15 := (get (fromJson (include "_shims.asnumeric" (dict "a" (list $v)))) "r") -}} -{{- $_ := ((index $_1909___ok_15 0) | float64) -}} -{{- $ok_15 := (index $_1909___ok_15 1) -}} +{{- $_1921___ok_15 := (get (fromJson (include "_shims.asnumeric" (dict "a" (list $v)))) "r") -}} +{{- $_ := ((index $_1921___ok_15 0) | float64) -}} +{{- $ok_15 := (index $_1921___ok_15 1) -}} {{- if $ok_15 -}} {{- $_ := (set $result $k $v) -}} {{- else -}}{{- if (kindIs "bool" $v) -}} @@ -1414,9 +1424,9 @@ {{- $_is_returning := false -}} {{- $result := (dict) -}} {{- range $k, $v := $c -}} -{{- $_1929_b_16_ok_17 := (get (fromJson (include "_shims.typetest" (dict "a" (list "bool" $v false)))) "r") -}} -{{- $b_16 := (index $_1929_b_16_ok_17 0) -}} -{{- $ok_17 := (index $_1929_b_16_ok_17 1) -}} +{{- $_1941_b_16_ok_17 := (get (fromJson (include "_shims.typetest" (dict "a" (list "bool" $v false)))) "r") -}} +{{- $b_16 := (index $_1941_b_16_ok_17 0) -}} +{{- $ok_17 := (index $_1941_b_16_ok_17 1) -}} {{- if $ok_17 -}} {{- $_ := (set $result $k $b_16) -}} {{- continue -}} @@ -1459,15 +1469,15 @@ {{- $config := (index .a 1) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $_1974___hasAccessKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_access_key" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1974___hasAccessKey 0) -}} -{{- $hasAccessKey := (index $_1974___hasAccessKey 1) -}} -{{- $_1975___hasSecretKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_secret_key" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1975___hasSecretKey 0) -}} -{{- $hasSecretKey := (index $_1975___hasSecretKey 1) -}} -{{- $_1976___hasSharedKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_azure_shared_key" (coalesce nil))))) "r") -}} -{{- $_ := (index $_1976___hasSharedKey 0) -}} -{{- $hasSharedKey := (index $_1976___hasSharedKey 1) -}} +{{- $_1986___hasAccessKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_access_key" (coalesce nil))))) "r") -}} +{{- $_ := (index $_1986___hasAccessKey 0) -}} +{{- $hasAccessKey := (index $_1986___hasAccessKey 1) -}} +{{- $_1987___hasSecretKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_secret_key" (coalesce nil))))) "r") -}} +{{- $_ := (index $_1987___hasSecretKey 0) -}} +{{- $hasSecretKey := (index $_1987___hasSecretKey 1) -}} +{{- $_1988___hasSharedKey := (get (fromJson (include "_shims.dicttest" (dict "a" (list $config "cloud_storage_azure_shared_key" (coalesce nil))))) "r") -}} +{{- $_ := (index $_1988___hasSharedKey 0) -}} +{{- $hasSharedKey := (index $_1988___hasSharedKey 1) -}} {{- $envvars := (coalesce nil) -}} {{- if (and (not $hasAccessKey) (get (fromJson (include "redpanda.SecretRef.IsValid" (dict "a" (list $tsc.accessKey)))) "r")) -}} {{- $envvars = (concat (default (list) $envvars) (list (mustMergeOverwrite (dict "name" "") (dict "name" "REDPANDA_CLOUD_STORAGE_ACCESS_KEY" "valueFrom" (get (fromJson (include "redpanda.SecretRef.AsSource" (dict "a" (list $tsc.accessKey)))) "r"))))) -}} @@ -1490,12 +1500,12 @@ {{- $c := (index .a 0) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $_2012___containerExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_container" (coalesce nil))))) "r") -}} -{{- $_ := (index $_2012___containerExists 0) -}} -{{- $containerExists := (index $_2012___containerExists 1) -}} -{{- $_2013___accountExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_storage_account" (coalesce nil))))) "r") -}} -{{- $_ := (index $_2013___accountExists 0) -}} -{{- $accountExists := (index $_2013___accountExists 1) -}} +{{- $_2024___containerExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_container" (coalesce nil))))) "r") -}} +{{- $_ := (index $_2024___containerExists 0) -}} +{{- $containerExists := (index $_2024___containerExists 1) -}} +{{- $_2025___accountExists := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c "cloud_storage_azure_storage_account" (coalesce nil))))) "r") -}} +{{- $_ := (index $_2025___accountExists 0) -}} +{{- $accountExists := (index $_2025___accountExists 1) -}} {{- $_is_returning = true -}} {{- (dict "r" (and $containerExists $accountExists)) | toJson -}} {{- break -}} @@ -1506,9 +1516,9 @@ {{- $c := (index .a 0) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $_2018_value_ok := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c `cloud_storage_cache_size` (coalesce nil))))) "r") -}} -{{- $value := (index $_2018_value_ok 0) -}} -{{- $ok := (index $_2018_value_ok 1) -}} +{{- $_2030_value_ok := (get (fromJson (include "_shims.dicttest" (dict "a" (list $c `cloud_storage_cache_size` (coalesce nil))))) "r") -}} +{{- $value := (index $_2030_value_ok 0) -}} +{{- $ok := (index $_2030_value_ok 1) -}} {{- if (not $ok) -}} {{- $_is_returning = true -}} {{- (dict "r" (coalesce nil)) | toJson -}} diff --git a/charts/redpanda/service.loadbalancer.go b/charts/redpanda/service.loadbalancer.go index adebe510e..2fc99a395 100644 --- a/charts/redpanda/service.loadbalancer.go +++ b/charts/redpanda/service.loadbalancer.go @@ -21,28 +21,27 @@ import ( ) // dedicatedListenerNames returns the set of external listener names that have -// per-listener annotations configured on any protocol. These listeners will get -// their own dedicated LoadBalancer Service per broker instead of sharing the -// default one. +// a per-listener Type configured on any protocol. These listeners will get +// their own dedicated Service per broker instead of sharing the default one. func dedicatedListenerNames(listeners *Listeners) map[string]bool { dedicated := map[string]bool{} for name, l := range helmette.SortedMap(listeners.Admin.External) { - if len(l.Annotations) > 0 { + if l.HasDedicatedService() { dedicated[name] = true } } for name, l := range helmette.SortedMap(listeners.Kafka.External) { - if len(l.Annotations) > 0 { + if l.HasDedicatedService() { dedicated[name] = true } } for name, l := range helmette.SortedMap(listeners.HTTP.External) { - if len(l.Annotations) > 0 { + if l.HasDedicatedService() { dedicated[name] = true } } for name, l := range helmette.SortedMap(listeners.SchemaRegistry.External) { - if len(l.Annotations) > 0 { + if l.HasDedicatedService() { dedicated[name] = true } } @@ -111,6 +110,33 @@ func dedicatedListenerSourceRanges(listeners *Listeners, listenerName string) [] return nil } +// dedicatedListenerServiceType returns the Service type for a named listener. +// Uses the first non-nil Type found across protocols. +func dedicatedListenerServiceType(listeners *Listeners, listenerName string) corev1.ServiceType { + for name, l := range helmette.SortedMap(listeners.Kafka.External) { + if name == listenerName && l.Type != nil { + return *l.Type + } + } + for name, l := range helmette.SortedMap(listeners.Admin.External) { + if name == listenerName && l.Type != nil { + return *l.Type + } + } + for name, l := range helmette.SortedMap(listeners.HTTP.External) { + if name == listenerName && l.Type != nil { + return *l.Type + } + } + for name, l := range helmette.SortedMap(listeners.SchemaRegistry.External) { + if name == listenerName && l.Type != nil { + return *l.Type + } + } + // Fallback to LoadBalancer if somehow we get here. + return corev1.ServiceTypeLoadBalancer +} + func LoadBalancerServices(state *RenderState) []*corev1.Service { // This is technically a divergence from previous behavior but this matches // the NodePort's check and is more reasonable. @@ -138,7 +164,7 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { pods = append(pods, PodNames(state, set)...) } - // Identify listeners that should get their own dedicated LB Service. + // Identify listeners that should get their own dedicated Service. dedicated := dedicatedListenerNames(&state.Values.Listeners) for i, podname := range pods { @@ -175,7 +201,7 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { podSelector["statefulset.kubernetes.io/pod-name"] = podname - // Default shared LB: includes all listeners that do NOT have dedicated annotations. + // Default shared LB: includes all listeners that do NOT have a dedicated Service type. var ports []corev1.ServicePort ports = append(ports, state.Values.Listeners.Admin.ServicePortsExcludingListeners("admin", &state.Values.External, dedicated)...) ports = append(ports, state.Values.Listeners.Kafka.ServicePortsExcludingListeners("kafka", &state.Values.External, dedicated)...) @@ -209,7 +235,7 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { services = append(services, svc) } - // Dedicated LBs: one per listener name that has annotations. + // Dedicated Services: one per listener name that has a Type set. for listenerName := range helmette.SortedMap(dedicated) { var dedicatedPorts []corev1.ServicePort dedicatedPorts = append(dedicatedPorts, state.Values.Listeners.Admin.ServicePortsForListener("admin", listenerName, &state.Values.External)...) @@ -226,6 +252,7 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { dedicatedAnnotations[k] = v } + svcType := dedicatedListenerServiceType(&state.Values.Listeners, listenerName) sourceRanges := dedicatedListenerSourceRanges(&state.Values.Listeners, listenerName) svc := &corev1.Service{ @@ -246,7 +273,7 @@ func LoadBalancerServices(state *RenderState) []*corev1.Service { PublishNotReadyAddresses: true, Selector: podSelector, SessionAffinity: corev1.ServiceAffinityNone, - Type: corev1.ServiceTypeLoadBalancer, + Type: svcType, }, } diff --git a/charts/redpanda/values.go b/charts/redpanda/values.go index 3c26cbaf5..d32f6c9bb 100644 --- a/charts/redpanda/values.go +++ b/charts/redpanda/values.go @@ -1824,19 +1824,30 @@ type ExternalListener[T ~string] struct { AuthenticationMethod *T `json:"authenticationMethod,omitempty"` PrefixTemplate *string `json:"prefixTemplate,omitempty"` - // Annotations, when set, causes this listener to be served by a dedicated - // per-broker LoadBalancer Service with these annotations, instead of sharing - // the default per-broker LoadBalancer. This enables use cases like having a - // private listener on one LB and a public listener on another, each with - // different cloud-provider annotations. + // Type, when set, causes this listener to be served by a dedicated + // per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + // instead of sharing the default per-broker Service. This enables use + // cases like having a private listener on an internal LB and a public + // listener on an internet-facing LB. When not set, the listener shares + // the default per-broker Service created from the global external config. + Type *corev1.ServiceType `json:"type,omitempty" jsonschema:"pattern=^(LoadBalancer|NodePort)$"` + + // Annotations sets annotations on the dedicated Service for this listener. + // Only takes effect when Type is set. Annotations map[string]string `json:"annotations,omitempty"` - // LoadBalancerSourceRanges, when set, restricts traffic to the dedicated - // LoadBalancer for this listener to the specified CIDRs. Only takes effect - // when Annotations is also set (i.e., when this listener has a dedicated LB). + // LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + // for this listener to the specified CIDRs. Only takes effect when Type is + // set to LoadBalancer. LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"` } +// HasDedicatedService returns true if this listener should get its own +// dedicated per-broker Service instead of sharing the default one. +func (l *ExternalListener[T]) HasDedicatedService() bool { + return l.Type != nil +} + func (l *ExternalListener[T]) AsString() ExternalListener[string] { var auth *string if l.AuthenticationMethod != nil { @@ -1852,6 +1863,7 @@ func (l *ExternalListener[T]) AsString() ExternalListener[string] { TLS: l.TLS, AuthenticationMethod: auth, PrefixTemplate: l.PrefixTemplate, + Type: l.Type, Annotations: l.Annotations, LoadBalancerSourceRanges: l.LoadBalancerSourceRanges, } diff --git a/charts/redpanda/values_partial.gen.go b/charts/redpanda/values_partial.gen.go index 09112dc60..9bba685e1 100644 --- a/charts/redpanda/values_partial.gen.go +++ b/charts/redpanda/values_partial.gen.go @@ -411,6 +411,7 @@ type PartialExternalListener[T ~string] struct { TLS *PartialExternalTLS "json:\"tls,omitempty\"" AuthenticationMethod *T "json:\"authenticationMethod,omitempty\"" PrefixTemplate *string "json:\"prefixTemplate,omitempty\"" + Type *corev1.ServiceType "json:\"type,omitempty\" jsonschema:\"pattern=^(LoadBalancer|NodePort)$\"" Annotations map[string]string "json:\"annotations,omitempty\"" LoadBalancerSourceRanges []string "json:\"loadBalancerSourceRanges,omitempty\"" } diff --git a/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go b/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go index 30a26d3fa..74fc54f6b 100644 --- a/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go +++ b/operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go @@ -974,13 +974,17 @@ type ExternalListener struct { AdvertisedPorts []int32 `json:"advertisedPorts,omitempty"` NodePort *int32 `json:"nodePort,omitempty"` - // Annotations, when set, causes this listener to be served by a dedicated - // per-broker LoadBalancer Service with these annotations, instead of sharing - // the default per-broker LoadBalancer. + // Type, when set, causes this listener to be served by a dedicated + // per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + // instead of sharing the default per-broker Service. + Type *corev1.ServiceType `json:"type,omitempty"` + + // Annotations sets annotations on the dedicated Service for this listener. + // Only takes effect when Type is set. Annotations map[string]string `json:"annotations,omitempty"` // LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer - // for this listener. Only takes effect when Annotations is also set. + // for this listener. Only takes effect when Type is set to LoadBalancer. LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"` } From dd5cdf54c6dd6c814261170e4aab7d03cadf0653 Mon Sep 17 00:00:00 2001 From: david-yu Date: Thu, 26 Mar 2026 14:18:16 -0700 Subject: [PATCH 4/6] fix: regenerate values.schema.json with current gen binary Rebuilds the gen binary to match CI and regenerates the schema. This removes the stale top-level gateway section and ensures the schema matches what CI produces. Co-Authored-By: Claude Opus 4.6 (1M context) --- charts/redpanda/chart/values.schema.json | 123 ++++++++++++----------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/charts/redpanda/chart/values.schema.json b/charts/redpanda/chart/values.schema.json index 382c76af0..99f03d0c5 100644 --- a/charts/redpanda/chart/values.schema.json +++ b/charts/redpanda/chart/values.schema.json @@ -3391,65 +3391,6 @@ "fullnameOverride": { "type": "string" }, - "gateway": { - "additionalProperties": false, - "properties": { - "annotations": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "enabled": { - "type": "boolean" - }, - "hostnames": { - "oneOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ] - }, - "parentRefs": { - "oneOf": [ - { - "items": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "sectionName": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - { - "type": "null" - } - ] - }, - "path": { - "type": "string" - }, - "pathType": { - "type": "string" - } - }, - "type": "object" - }, "global": { "type": "object" }, @@ -4690,6 +4631,12 @@ "minItems": 1, "type": "array" }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "authenticationMethod": { "oneOf": [ { @@ -4703,6 +4650,12 @@ "enabled": { "type": "boolean" }, + "loadBalancerSourceRanges": { + "items": { + "type": "string" + }, + "type": "array" + }, "nodePort": { "type": "integer" }, @@ -4764,6 +4717,10 @@ } }, "type": "object" + }, + "type": { + "pattern": "^(LoadBalancer|NodePort)$", + "type": "string" } }, "required": [ @@ -4877,6 +4834,12 @@ "minItems": 1, "type": "array" }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "authenticationMethod": { "oneOf": [ { @@ -4894,6 +4857,12 @@ "enabled": { "type": "boolean" }, + "loadBalancerSourceRanges": { + "items": { + "type": "string" + }, + "type": "array" + }, "nodePort": { "type": "integer" }, @@ -4955,6 +4924,10 @@ } }, "type": "object" + }, + "type": { + "pattern": "^(LoadBalancer|NodePort)$", + "type": "string" } }, "required": [ @@ -5069,6 +5042,12 @@ "minItems": 1, "type": "array" }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "authenticationMethod": { "oneOf": [ { @@ -5087,6 +5066,12 @@ "enabled": { "type": "boolean" }, + "loadBalancerSourceRanges": { + "items": { + "type": "string" + }, + "type": "array" + }, "nodePort": { "type": "integer" }, @@ -5148,6 +5133,10 @@ } }, "type": "object" + }, + "type": { + "pattern": "^(LoadBalancer|NodePort)$", + "type": "string" } }, "required": [ @@ -5327,6 +5316,12 @@ "minItems": 1, "type": "array" }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "authenticationMethod": { "oneOf": [ { @@ -5340,6 +5335,12 @@ "enabled": { "type": "boolean" }, + "loadBalancerSourceRanges": { + "items": { + "type": "string" + }, + "type": "array" + }, "nodePort": { "type": "integer" }, @@ -5401,6 +5402,10 @@ } }, "type": "object" + }, + "type": { + "pattern": "^(LoadBalancer|NodePort)$", + "type": "string" } }, "required": [ From ac29cd8261f32c0e60745fe10bdd9bf852529313 Mon Sep 17 00:00:00 2001 From: david-yu Date: Fri, 27 Mar 2026 08:52:10 -0700 Subject: [PATCH 5/6] fix: align generated files with controller-gen output Update generated files to match CI's controller-gen v0.20.1 output: - values.go: fix field alignment in ExternalListener.AsString() - crd-docs.adoc: add docs for type, annotations, loadBalancerSourceRanges - zz_generated.deepcopy.go: add deepcopy for new ExternalListener fields - cluster.redpanda.com_redpandas.yaml: add CRD schema for new fields Co-Authored-By: Claude Opus 4.6 (1M context) --- charts/redpanda/values.go | 18 +- .../redpanda/v1alpha2/testdata/crd-docs.adoc | 9 +- .../v1alpha2/zz_generated.deepcopy.go | 17 ++ .../bases/cluster.redpanda.com_redpandas.yaml | 160 ++++++++++++++++++ 4 files changed, 194 insertions(+), 10 deletions(-) diff --git a/charts/redpanda/values.go b/charts/redpanda/values.go index d32f6c9bb..bb421912a 100644 --- a/charts/redpanda/values.go +++ b/charts/redpanda/values.go @@ -1856,15 +1856,15 @@ func (l *ExternalListener[T]) AsString() ExternalListener[string] { } return ExternalListener[string]{ - Enabled: l.Enabled, - AdvertisedPorts: l.AdvertisedPorts, - Port: l.Port, - NodePort: l.NodePort, - TLS: l.TLS, - AuthenticationMethod: auth, - PrefixTemplate: l.PrefixTemplate, - Type: l.Type, - Annotations: l.Annotations, + Enabled: l.Enabled, + AdvertisedPorts: l.AdvertisedPorts, + Port: l.Port, + NodePort: l.NodePort, + TLS: l.TLS, + AuthenticationMethod: auth, + PrefixTemplate: l.PrefixTemplate, + Type: l.Type, + Annotations: l.Annotations, LoadBalancerSourceRanges: l.LoadBalancerSourceRanges, } } diff --git a/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc b/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc index f88dad793..852b29ba9 100644 --- a/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc +++ b/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc @@ -1229,7 +1229,14 @@ For historical backwards compatibility, this field is present on both + internal and external listeners. However, it is ignored when specified + on internal listeners. + | | | *`advertisedPorts`* __integer array__ | Specifies the network port that the external Service listens on. + | | -| *`nodePort`* __integer__ | | | +| *`nodePort`* __integer__ | | | +| *`type`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#servicetype-v1-core[$$ServiceType$$]__ | Type, when set, causes this listener to be served by a dedicated + +per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + +instead of sharing the default per-broker Service. + | | +| *`annotations`* __object (keys:string, values:string)__ | Annotations sets annotations on the dedicated Service for this listener. + +Only takes effect when Type is set. + | | +| *`loadBalancerSourceRanges`* __string array__ | LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + +for this listener. Only takes effect when Type is set to LoadBalancer. + | | |=== diff --git a/operator/api/redpanda/v1alpha2/zz_generated.deepcopy.go b/operator/api/redpanda/v1alpha2/zz_generated.deepcopy.go index 20f88b122..32b577bf5 100644 --- a/operator/api/redpanda/v1alpha2/zz_generated.deepcopy.go +++ b/operator/api/redpanda/v1alpha2/zz_generated.deepcopy.go @@ -1566,6 +1566,23 @@ func (in *ExternalListener) DeepCopyInto(out *ExternalListener) { *out = new(int32) **out = **in } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(v1.ServiceType) + **out = **in + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LoadBalancerSourceRanges != nil { + in, out := &in.LoadBalancerSourceRanges, &out.LoadBalancerSourceRanges + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalListener. diff --git a/operator/config/crd/bases/cluster.redpanda.com_redpandas.yaml b/operator/config/crd/bases/cluster.redpanda.com_redpandas.yaml index 21609c088..cd58855c8 100644 --- a/operator/config/crd/bases/cluster.redpanda.com_redpandas.yaml +++ b/operator/config/crd/bases/cluster.redpanda.com_redpandas.yaml @@ -1985,6 +1985,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -1996,6 +2003,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -2095,6 +2109,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -2218,6 +2238,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -2229,6 +2256,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -2328,6 +2362,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -2456,6 +2496,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -2467,6 +2514,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -2566,6 +2620,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -2777,6 +2837,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -2788,6 +2855,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -2887,6 +2961,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -35779,6 +35859,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -35790,6 +35877,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -35889,6 +35983,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -36012,6 +36112,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -36023,6 +36130,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -36122,6 +36236,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -36250,6 +36370,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -36261,6 +36388,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -36360,6 +36494,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object @@ -36571,6 +36711,13 @@ spec: format: int32 type: integer type: array + annotations: + additionalProperties: + type: string + description: |- + Annotations sets annotations on the dedicated Service for this listener. + Only takes effect when Type is set. + type: object appProtocol: type: string authenticationMethod: @@ -36582,6 +36729,13 @@ spec: description: Specifies whether this Listener is enabled. type: boolean + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + for this listener. Only takes effect when Type is set to LoadBalancer. + items: + type: string + type: array nodePort: format: int32 type: integer @@ -36681,6 +36835,12 @@ spec: x-kubernetes-map-type: atomic type: object type: object + type: + description: |- + Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + instead of sharing the default per-broker Service. + type: string type: object description: Defines settings for the external listeners. type: object From c89c5c966df28650eede6abdb1133608271cb061 Mon Sep 17 00:00:00 2001 From: david-yu Date: Fri, 27 Mar 2026 09:17:10 -0700 Subject: [PATCH 6/6] fix: add trailing spaces to crd-docs.adoc for generator compatibility Co-Authored-By: Claude Opus 4.6 (1M context) --- operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc b/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc index 852b29ba9..015d6d41d 100644 --- a/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc +++ b/operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc @@ -1229,14 +1229,14 @@ For historical backwards compatibility, this field is present on both + internal and external listeners. However, it is ignored when specified + on internal listeners. + | | | *`advertisedPorts`* __integer array__ | Specifies the network port that the external Service listens on. + | | -| *`nodePort`* __integer__ | | | +| *`nodePort`* __integer__ | | | | *`type`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#servicetype-v1-core[$$ServiceType$$]__ | Type, when set, causes this listener to be served by a dedicated + per-broker Service of the specified type (e.g., LoadBalancer, NodePort) + -instead of sharing the default per-broker Service. + | | +instead of sharing the default per-broker Service. + | | | *`annotations`* __object (keys:string, values:string)__ | Annotations sets annotations on the dedicated Service for this listener. + -Only takes effect when Type is set. + | | +Only takes effect when Type is set. + | | | *`loadBalancerSourceRanges`* __string array__ | LoadBalancerSourceRanges restricts traffic to the dedicated LoadBalancer + -for this listener. Only takes effect when Type is set to LoadBalancer. + | | +for this listener. Only takes effect when Type is set to LoadBalancer. + | | |===