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 @@ -115,6 +115,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
43 changes: 42 additions & 1 deletion pkg/kgateway/deployer/gateway_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ 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 @@ -29,6 +31,12 @@ 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 @@ -400,6 +408,30 @@ 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 @@ -477,7 +509,16 @@ 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 {
var err error
loadBalancerIP, err = extractLoadBalancerIP(gw)
if err != nil {
return nil, err
}
}
gateway.Service = deployer.GetServiceValues(svcConfig, loadBalancerIP)
// serviceaccount values
gateway.ServiceAccount = deployer.GetServiceAccountValues(svcAccountConfig)
// pod template values
Expand Down
137 changes: 137 additions & 0 deletions pkg/kgateway/deployer/gateway_parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,140 @@ 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
wantErr error
}{
{
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"),
wantErr: nil,
},
{
name: "single valid IPv6 address",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "2001:db8::1"},
},
want: ptr.To("2001:db8::1"),
wantErr: nil,
},
{
name: "nil address type defaults to IPAddressType",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "192.0.2.1"},
},
want: ptr.To("192.0.2.1"),
wantErr: nil,
},
{
name: "empty addresses array",
addresses: []gwv1.GatewaySpecAddress{},
want: 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"},
},
want: 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"},
},
want: nil,
wantErr: ErrMultipleAddresses,
},
{
name: "single hostname address returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
},
want: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "single invalid IP address returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "not-an-ip"},
},
want: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "single invalid IP address format returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: ptr.To(gwv1.IPAddressType), Value: "256.256.256.256"},
},
want: nil,
wantErr: ErrNoValidIPAddress,
},
{
name: "nil type with valid IP returns IP",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "203.0.113.10"},
},
want: ptr.To("203.0.113.10"),
wantErr: nil,
},
{
name: "nil type with invalid IP returns error",
addresses: []gwv1.GatewaySpecAddress{
{Type: nil, Value: "invalid"},
},
want: 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"},
},
want: nil,
wantErr: ErrMultipleAddresses,
},
}

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, err := extractLoadBalancerIP(gw)
if tt.wantErr != nil {
assert.Error(t, err, "expected error")
assert.ErrorIs(t, err, tt.wantErr, "error should be ErrMultipleAddresses")
assert.Nil(t, got, "expected nil IP when error occurs")
} else {
assert.NoError(t, err, "unexpected error")
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 @@ -121,6 +121,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