Skip to content
44 changes: 42 additions & 2 deletions pkg/deployer/values_helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package deployer

import (
"errors"
"fmt"
"net/netip"
"regexp"
"sort"
"strings"
Expand All @@ -18,6 +20,14 @@ import (
"github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir"
)

var (
// ErrMultipleAddresses is returned when multiple addresses are specified in Gateway.spec.addresses
ErrMultipleAddresses = errors.New("multiple addresses given, only one address is supported")

// ErrNoValidIPAddress is returned when no valid IP address is found in Gateway.spec.addresses
ErrNoValidIPAddress = errors.New("no valid IP address found in Gateway.spec.addresses")
)

// This file contains helper functions that generate helm values in the format needed
// by the deployer.

Expand Down Expand Up @@ -104,7 +114,7 @@ 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, loadBalancerIP *string) *HelmService {
func GetServiceValues(svcConfig *kgateway.Service) *HelmService {
// convert the service type enum to its string representation;
// if type is not set, it will default to 0 ("ClusterIP")
var svcType *string
Expand All @@ -126,13 +136,43 @@ func GetServiceValues(svcConfig *kgateway.Service, loadBalancerIP *string) *Helm
return &HelmService{
Type: svcType,
ClusterIP: clusterIP,
LoadBalancerIP: loadBalancerIP,
ExtraAnnotations: extraAnnotations,
ExtraLabels: extraLabels,
ExternalTrafficPolicy: externalTrafficPolicy,
}
}

// SetLoadBalancerIPFromGateway extracts the IP address from Gateway.spec.addresses
// and sets it on the HelmService if the service type is LoadBalancer.
// Only sets the IP if exactly one valid IP address is found in Gateway.spec.addresses.
// Returns an error if more than one address is specified or no valid IP address is found.
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function documentation is incomplete. It states that an error is returned "if more than one address is specified or no valid IP address is found", but it doesn't clarify what happens when a single address of a non-IP type (e.g., HostnameAddressType) is provided. The current implementation returns ErrNoValidIPAddress in this case. Consider updating the documentation to explicitly state: "Returns an error if more than one address is specified, if the single address is not of type IPAddress, or if the IP address value is invalid."

Suggested change
// Returns an error if more than one address is specified or no valid IP address is found.
// Returns an error if more than one address is specified, if the single address is not of type IPAddress,
// or if the IP address value is invalid.

Copilot uses AI. Check for mistakes.
func SetLoadBalancerIPFromGateway(gw *gwv1.Gateway, svc *HelmService) error {
// Only extract IP if service type is LoadBalancer
if svc.Type == nil || *svc.Type != string(corev1.ServiceTypeLoadBalancer) {
return nil
}

if len(gw.Spec.Addresses) == 0 {
return nil
}

if len(gw.Spec.Addresses) > 1 {
return fmt.Errorf("%w: gateway %s/%s has %d addresses", ErrMultipleAddresses, gw.Namespace, gw.Name, len(gw.Spec.Addresses))
}

addr := gw.Spec.Addresses[0]
// 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() {
svc.LoadBalancerIP = &addr.Value
return nil
}
}

return fmt.Errorf("%w: gateway %s/%s has no valid IP address", ErrNoValidIPAddress, gw.Namespace, gw.Name)
}

// Convert service account values from GatewayParameters into helm values to be used by the deployer.
func GetServiceAccountValues(svcAccountConfig *kgateway.ServiceAccount) *HelmServiceAccount {
return &HelmServiceAccount{
Expand Down
176 changes: 176 additions & 0 deletions pkg/deployer/values_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func TestComponentLogLevelsToString(t *testing.T) {
Expand Down Expand Up @@ -56,3 +60,175 @@ func TestComponentLogLevelsToString(t *testing.T) {
})
}
}

func TestSetLoadBalancerIPFromGateway(t *testing.T) {
tests := []struct {
name string
addresses []gwv1.GatewaySpecAddress
serviceType *string
wantIP *string
wantErr error
}{
{
name: "single valid IPv4 address with LoadBalancer service",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: ptr.To("203.0.113.10"),
wantErr: nil,
},
{
name: "single valid IPv6 address with LoadBalancer service",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "2001:db8::1"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: ptr.To("2001:db8::1"),
wantErr: nil,
},
{
name: "nil address type defaults to IPAddressType",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "192.0.2.1"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: ptr.To("192.0.2.1"),
wantErr: nil,
},
{
name: "empty addresses array with LoadBalancer service",
addresses: []gwv1.GatewaySpecAddress{},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: nil,
},
{
name: "multiple valid IP addresses returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.11"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrMultipleAddresses,
},
{
name: "multiple addresses with mixed types returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrMultipleAddresses,
},
{
name: "single hostname address returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "single invalid IP address returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "not-an-ip"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "single invalid IP address format returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "256.256.256.256"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "nil type with valid IP returns IP",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "203.0.113.10"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: ptr.To("203.0.113.10"),
wantErr: nil,
},
{
name: "nil type with invalid IP returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "invalid"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "three addresses returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.11"},
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.12"},
},
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
wantIP: nil,
wantErr: ErrMultipleAddresses,
},
{
name: "valid IP with ClusterIP service does not set IP",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
serviceType: ptr.To(string(corev1.ServiceTypeClusterIP)),
wantIP: nil,
wantErr: nil,
},
{
name: "valid IP with nil service type does not set IP",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
},
serviceType: nil,
wantIP: nil,
wantErr: nil,
},
}

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,
},
}

svc := &HelmService{
Type: tt.serviceType,
}

err := SetLoadBalancerIPFromGateway(gw, svc)
if tt.wantErr != nil {
assert.Error(t, err, "expected error")
assert.ErrorIs(t, err, tt.wantErr, "error type mismatch")
assert.Nil(t, svc.LoadBalancerIP, "expected nil IP when error occurs")
} else {
assert.NoError(t, err, "unexpected error")
if tt.wantIP == nil {
assert.Nil(t, svc.LoadBalancerIP, "expected nil but got %v", svc.LoadBalancerIP)
} else {
assert.NotNil(t, svc.LoadBalancerIP, "expected non-nil IP")
assert.Equal(t, *tt.wantIP, *svc.LoadBalancerIP, "IP address mismatch")
}
}
})
}
}
45 changes: 4 additions & 41 deletions pkg/kgateway/deployer/gateway_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import (
"errors"
"fmt"
"log/slog"
"net/netip"
"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 All @@ -31,12 +29,6 @@ var (

// ErrNotFound is returned when a requested resource is not found
ErrNotFound = errors.New("resource not found")

// ErrMultipleAddresses is returned when multiple addresses are specified in Gateway.spec.addresses
ErrMultipleAddresses = errors.New("multiple addresses given, only one address is supported")

// ErrNoValidIPAddress is returned when no valid IP address is found in Gateway.spec.addresses
ErrNoValidIPAddress = errors.New("no valid IP address found in Gateway.spec.addresses")
)

func NewGatewayParameters(cli apiclient.Client, inputs *deployer.Inputs) *GatewayParameters {
Expand Down Expand Up @@ -408,30 +400,6 @@ func (k *kgatewayParameters) getGatewayParametersForGatewayClass(gwc *gwv1.Gatew
return mergedGwp, nil
}

// extractLoadBalancerIP extracts the IP address from Gateway.spec.addresses
// where the address type is IPAddressType.
// Returns an error if more than one address is specified or no valid IP address is found.
func extractLoadBalancerIP(gw *gwv1.Gateway) (*string, error) {
if len(gw.Spec.Addresses) == 0 {
return nil, nil
}

if len(gw.Spec.Addresses) > 1 {
return nil, fmt.Errorf("%w: gateway %s/%s has %d addresses", ErrMultipleAddresses, gw.Namespace, gw.Name, len(gw.Spec.Addresses))
}

addr := gw.Spec.Addresses[0]
// 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, nil
}
}

return nil, fmt.Errorf("%w: gateway %s/%s has no valid IP address", ErrNoValidIPAddress, gw.Namespace, gw.Name)
}

func (k *kgatewayParameters) getValues(gw *gwv1.Gateway, gwParam *kgateway.GatewayParameters) (*deployer.HelmConfig, error) {
irGW := deployer.GetGatewayIR(gw, k.inputs.CommonCollections)
// kgatewayParameters is only used for envoy gateways (agentgateway uses agentgatewayParametersHelmValuesGenerator)
Expand Down Expand Up @@ -509,16 +477,11 @@ func (k *kgatewayParameters) getValues(gw *gwv1.Gateway, gwParam *kgateway.Gatew
gateway.Strategy = deployConfig.GetStrategy()

// service values
// Extract loadBalancerIP from Gateway.spec.addresses if service type is LoadBalancer
var loadBalancerIP *string
if svcConfig != nil && svcConfig.GetType() != nil && *svcConfig.GetType() == corev1.ServiceTypeLoadBalancer {
var err error
loadBalancerIP, err = extractLoadBalancerIP(gw)
if err != nil {
return nil, err
}
gateway.Service = deployer.GetServiceValues(svcConfig)
// Extract loadBalancerIP from Gateway.spec.addresses and set it on the service if service type is LoadBalancer
if err := deployer.SetLoadBalancerIPFromGateway(gw, gateway.Service); err != nil {
return nil, err
}
gateway.Service = deployer.GetServiceValues(svcConfig, loadBalancerIP)
// serviceaccount values
gateway.ServiceAccount = deployer.GetServiceAccountValues(svcAccountConfig)
// pod template values
Expand Down
Loading
Loading