Skip to content
1 change: 1 addition & 0 deletions pkg/deployer/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type HelmImage struct {
type HelmService struct {
Type *string `json:"type,omitempty"`
ClusterIP *string `json:"clusterIP,omitempty"`
LoadBalancerIP *string `json:"loadBalancerIP,omitempty"`
ExtraAnnotations map[string]string `json:"extraAnnotations,omitempty"`
ExtraLabels map[string]string `json:"extraLabels,omitempty"`
ExternalTrafficPolicy *string `json:"externalTrafficPolicy,omitempty"`
Expand Down
27 changes: 20 additions & 7 deletions pkg/deployer/values_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,32 @@ func AppendPortValue(gwPorts []HelmPort, port int32, name string, gwp *kgateway.
}

// Convert service values from GatewayParameters into helm values to be used by the deployer.
func GetServiceValues(svcConfig *kgateway.Service) *HelmService {
func GetServiceValues(svcConfig *kgateway.Service, loadBalancerIP *string) *HelmService {
// convert the service type enum to its string representation;
// if type is not set, it will default to 0 ("ClusterIP")
var svcType *string
if svcConfig.GetType() != nil {
svcType = ptr.To(string(*svcConfig.GetType()))
var clusterIP *string
var extraAnnotations map[string]string
var extraLabels map[string]string
var externalTrafficPolicy *string

if svcConfig != nil {
if svcConfig.GetType() != nil {
svcType = ptr.To(string(*svcConfig.GetType()))
}
clusterIP = svcConfig.GetClusterIP()
extraAnnotations = svcConfig.GetExtraAnnotations()
extraLabels = svcConfig.GetExtraLabels()
externalTrafficPolicy = svcConfig.GetExternalTrafficPolicy()
}

return &HelmService{
Type: svcType,
ClusterIP: svcConfig.GetClusterIP(),
ExtraAnnotations: svcConfig.GetExtraAnnotations(),
ExtraLabels: svcConfig.GetExtraLabels(),
ExternalTrafficPolicy: svcConfig.GetExternalTrafficPolicy(),
ClusterIP: clusterIP,
LoadBalancerIP: loadBalancerIP,
ExtraAnnotations: extraAnnotations,
ExtraLabels: extraLabels,
ExternalTrafficPolicy: externalTrafficPolicy,
}
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/kgateway/deployer/agentgateway_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,8 @@ func (g *agentgatewayParametersHelmValuesGenerator) applyGatewayParametersToHelm
}

svcConfig := gwp.Spec.Kube.GetService()
vals.Gateway.Service = deployer.GetServiceValues(svcConfig)
// TODO: extract loadBalancerIP from Gateway.spec.addresses if service type is LoadBalancer
vals.Gateway.Service = deployer.GetServiceValues(svcConfig, nil)

svcAccountConfig := gwp.Spec.Kube.GetServiceAccount()
vals.Gateway.ServiceAccount = deployer.GetServiceAccountValues(svcAccountConfig)
Expand Down
42 changes: 41 additions & 1 deletion pkg/kgateway/deployer/gateway_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"errors"
"fmt"
"log/slog"
"net/netip"
"os"
"strings"

"helm.sh/helm/v3/pkg/chart"
"istio.io/istio/pkg/kube/kclient"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -350,6 +352,39 @@ func (k *kgatewayParameters) getGatewayParametersForGatewayClass(gwc *gwv1.Gatew
return mergedGwp, nil
}

// extractLoadBalancerIP extracts the first IP address from Gateway.spec.addresses
// where the address type is IPAddressType. Returns nil if no valid IP address is found.
// If multiple IP addresses are provided, uses the first one and logs a warning.
func extractLoadBalancerIP(gw *gwv1.Gateway) *string {
if len(gw.Spec.Addresses) == 0 {
return nil
}

if len(gw.Spec.Addresses) > 1 {
slog.Warn("multiple addresses found in Gateway.spec.addresses, using first valid IP address",
"gateway", fmt.Sprintf("%s/%s", gw.Namespace, gw.Name),
"count", len(gw.Spec.Addresses),
)
}

for _, addr := range gw.Spec.Addresses {
// IPAddressType or nil (defaults to IPAddressType per Gateway API spec)
if addr.Type == nil || *addr.Type == gwv1.IPAddressType {
// Validate IP format
if parsedIP, err := netip.ParseAddr(addr.Value); err == nil && parsedIP.IsValid() {
return &addr.Value
}
// Log warning for invalid IP but continue searching
slog.Warn("invalid IP address in Gateway.spec.addresses, skipping", "value", addr.Value)
}
}

slog.Error("no valid IP address found in Gateway.spec.addresses",
"gateway", fmt.Sprintf("%s/%s", gw.Namespace, gw.Name),
)
return nil
}

func (k *kgatewayParameters) getValues(gw *gwv1.Gateway, gwParam *kgateway.GatewayParameters) (*deployer.HelmConfig, error) {
irGW := deployer.GetGatewayIR(gw, k.inputs.CommonCollections)
ports := deployer.GetPortsValues(irGW, gwParam, irGW.ControllerName == k.inputs.AgentgatewayControllerName)
Expand Down Expand Up @@ -436,7 +471,12 @@ func (k *kgatewayParameters) getValues(gw *gwv1.Gateway, gwParam *kgateway.Gatew
gateway.Strategy = deployConfig.GetStrategy()

// service values
gateway.Service = deployer.GetServiceValues(svcConfig)
// Extract loadBalancerIP from Gateway.spec.addresses if service type is LoadBalancer
var loadBalancerIP *string
if svcConfig != nil && svcConfig.GetType() != nil && *svcConfig.GetType() == corev1.ServiceTypeLoadBalancer {
loadBalancerIP = extractLoadBalancerIP(gw)
}
gateway.Service = deployer.GetServiceValues(svcConfig, loadBalancerIP)
// serviceaccount values
gateway.ServiceAccount = deployer.GetServiceAccountValues(svcAccountConfig)
// pod template values
Expand Down
130 changes: 130 additions & 0 deletions pkg/kgateway/deployer/gateway_parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,133 @@ func newCommonCols(t test.Failer, initObjs ...client.Object) *collections.Common
gateways.Gateways.WaitUntilSynced(ctx.Done())
return commonCols
}

func TestExtractLoadBalancerIP(t *testing.T) {
tests := []struct {
name string
addresses []gwv1.GatewaySpecAddress
want *string
}{
{
name: "single valid IPv4 address",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
want: ptr.To("203.0.113.10"),
},
{
name: "single valid IPv6 address",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "2001:db8::1"},
},
want: ptr.To("2001:db8::1"),
},
{
name: "nil address type defaults to IPAddressType",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "192.0.2.1"},
},
want: ptr.To("192.0.2.1"),
},
{
name: "empty addresses array",
addresses: []gwv1.GatewaySpecAddress{},
want: nil,
},
{
name: "multiple valid IP addresses uses first one",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.11"},
},
want: ptr.To("203.0.113.10"),
},
{
name: "multiple IP addresses with invalid format skips invalid and uses first valid",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "invalid-ip"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
want: ptr.To("203.0.113.10"),
},
{
name: "mixed address types uses first IP address",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
want: ptr.To("203.0.113.10"),
},
{
name: "all hostname addresses returns nil",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
{Type: ptr.To(gwv1.HostnameAddressType), Value: "test.example.com"},
},
want: nil,
},
{
name: "all invalid IP addresses returns nil",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "not-an-ip"},
{Type: ptr.To(gwv1.IPAddressType), Value: "256.256.256.256"},
},
want: nil,
},
{
name: "nil type with invalid IP skips and continues",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "invalid"},
{Type: nil, Value: "203.0.113.10"},
},
want: ptr.To("203.0.113.10"),
},
{
name: "IPv4 and IPv6 mixed uses first valid",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
{Type: ptr.To(gwv1.IPAddressType), Value: "2001:db8::1"},
},
want: ptr.To("203.0.113.10"),
},
{
name: "IPv6 first in multiple addresses",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "2001:db8::1"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
want: ptr.To("2001:db8::1"),
},
{
name: "hostname before IP address skips hostname",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.11"},
},
want: ptr.To("203.0.113.10"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gw := &gwv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gateway",
Namespace: "default",
},
Spec: gwv1.GatewaySpec{
Addresses: tt.addresses,
},
}

got := extractLoadBalancerIP(gw)
if tt.want == nil {
assert.Nil(t, got, "expected nil but got %v", got)
} else {
assert.NotNil(t, got, "expected non-nil result")
assert.Equal(t, *tt.want, *got, "IP address mismatch")
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/kgateway/helm/agentgateway/templates/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ spec:
{{- with $gateway.service.clusterIP }}
clusterIP: {{ . }}
{{- end }}
{{- with $gateway.service.loadBalancerIP }}
loadBalancerIP: {{ . }}
{{- end }}
ports:
{{- range $p := $gateway.ports }}
- name: {{ $p.name }}
Expand Down
3 changes: 3 additions & 0 deletions pkg/kgateway/helm/envoy/templates/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ spec:
{{- with $gateway.service.clusterIP }}
clusterIP: {{ . }}
{{- end }}
{{- with $gateway.service.loadBalancerIP }}
loadBalancerIP: {{ . }}
{{- end }}
ports:
{{- range $p := $gateway.ports }}
- name: {{ $p.name }}
Expand Down
4 changes: 4 additions & 0 deletions test/deployer/internal_helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBtestcertdata
Name: "envoy-infrastructure",
InputFile: "envoy-infrastructure",
},
{
Name: "gateway with static IP address",
InputFile: "loadbalancer-static-ip",
},
{
Name: "agentgateway-params-primary",
InputFile: "agentgateway-params-primary",
Expand Down
Loading
Loading