Skip to content

Commit 630e261

Browse files
authored
feat: set loadbalancerIP from Gateway.spec.addresses (#13070)
Signed-off-by: omar <[email protected]>
1 parent 2425a1f commit 630e261

File tree

9 files changed

+615
-6
lines changed

9 files changed

+615
-6
lines changed

pkg/deployer/values.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ type HelmImage struct {
115115
type HelmService struct {
116116
Type *string `json:"type,omitempty"`
117117
ClusterIP *string `json:"clusterIP,omitempty"`
118+
LoadBalancerIP *string `json:"loadBalancerIP,omitempty"`
118119
ExtraAnnotations map[string]string `json:"extraAnnotations,omitempty"`
119120
ExtraLabels map[string]string `json:"extraLabels,omitempty"`
120121
ExternalTrafficPolicy *string `json:"externalTrafficPolicy,omitempty"`

pkg/deployer/values_helpers.go

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package deployer
22

33
import (
4+
"errors"
45
"fmt"
6+
"net/netip"
57
"regexp"
68
"sort"
79
"strings"
@@ -18,6 +20,14 @@ import (
1820
"github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir"
1921
)
2022

23+
var (
24+
// ErrMultipleAddresses is returned when multiple addresses are specified in Gateway.spec.addresses
25+
ErrMultipleAddresses = errors.New("multiple addresses given, only one address is supported")
26+
27+
// ErrNoValidIPAddress is returned when no valid IP address is found in Gateway.spec.addresses
28+
ErrNoValidIPAddress = errors.New("IP address in Gateway.spec.addresses not valid")
29+
)
30+
2131
// This file contains helper functions that generate helm values in the format needed
2232
// by the deployer.
2333

@@ -108,16 +118,59 @@ func GetServiceValues(svcConfig *kgateway.Service) *HelmService {
108118
// convert the service type enum to its string representation;
109119
// if type is not set, it will default to 0 ("ClusterIP")
110120
var svcType *string
111-
if svcConfig.GetType() != nil {
112-
svcType = ptr.To(string(*svcConfig.GetType()))
121+
var clusterIP *string
122+
var extraAnnotations map[string]string
123+
var extraLabels map[string]string
124+
var externalTrafficPolicy *string
125+
126+
if svcConfig != nil {
127+
if svcConfig.GetType() != nil {
128+
svcType = ptr.To(string(*svcConfig.GetType()))
129+
}
130+
clusterIP = svcConfig.GetClusterIP()
131+
extraAnnotations = svcConfig.GetExtraAnnotations()
132+
extraLabels = svcConfig.GetExtraLabels()
133+
externalTrafficPolicy = svcConfig.GetExternalTrafficPolicy()
113134
}
135+
114136
return &HelmService{
115137
Type: svcType,
116-
ClusterIP: svcConfig.GetClusterIP(),
117-
ExtraAnnotations: svcConfig.GetExtraAnnotations(),
118-
ExtraLabels: svcConfig.GetExtraLabels(),
119-
ExternalTrafficPolicy: svcConfig.GetExternalTrafficPolicy(),
138+
ClusterIP: clusterIP,
139+
ExtraAnnotations: extraAnnotations,
140+
ExtraLabels: extraLabels,
141+
ExternalTrafficPolicy: externalTrafficPolicy,
142+
}
143+
}
144+
145+
// SetLoadBalancerIPFromGateway extracts the IP address from Gateway.spec.addresses
146+
// and sets it on the HelmService if the service type is LoadBalancer.
147+
// Only sets the IP if exactly one valid IP address is found in Gateway.spec.addresses.
148+
// Returns an error if more than one address is specified or no valid IP address is found.
149+
func SetLoadBalancerIPFromGateway(gw *gwv1.Gateway, svc *HelmService) error {
150+
// Only extract IP if service type is LoadBalancer
151+
if svc.Type == nil || *svc.Type != string(corev1.ServiceTypeLoadBalancer) {
152+
return nil
120153
}
154+
155+
if len(gw.Spec.Addresses) == 0 {
156+
return nil
157+
}
158+
159+
if len(gw.Spec.Addresses) > 1 {
160+
return fmt.Errorf("%w: gateway %s/%s has %d addresses", ErrMultipleAddresses, gw.Namespace, gw.Name, len(gw.Spec.Addresses))
161+
}
162+
163+
addr := gw.Spec.Addresses[0]
164+
// IPAddressType or nil (defaults to IPAddressType per Gateway API spec)
165+
if addr.Type == nil || *addr.Type == gwv1.IPAddressType {
166+
// Validate IP format
167+
if parsedIP, err := netip.ParseAddr(addr.Value); err == nil && parsedIP.IsValid() {
168+
svc.LoadBalancerIP = &addr.Value
169+
return nil
170+
}
171+
}
172+
173+
return fmt.Errorf("%w: gateway %s/%s has no valid IP address", ErrNoValidIPAddress, gw.Namespace, gw.Name)
121174
}
122175

123176
// Convert service account values from GatewayParameters into helm values to be used by the deployer.

pkg/deployer/values_helpers_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/assert"
7+
corev1 "k8s.io/api/core/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/utils/ptr"
10+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
711
)
812

913
func TestComponentLogLevelsToString(t *testing.T) {
@@ -56,3 +60,175 @@ func TestComponentLogLevelsToString(t *testing.T) {
5660
})
5761
}
5862
}
63+
64+
func TestSetLoadBalancerIPFromGateway(t *testing.T) {
65+
tests := []struct {
66+
name string
67+
addresses []gwv1.GatewaySpecAddress
68+
serviceType *string
69+
wantIP *string
70+
wantErr error
71+
}{
72+
{
73+
name: "single valid IPv4 address with LoadBalancer service",
74+
addresses: []gwv1.GatewaySpecAddress{
75+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
76+
},
77+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
78+
wantIP: ptr.To("203.0.113.10"),
79+
wantErr: nil,
80+
},
81+
{
82+
name: "single valid IPv6 address with LoadBalancer service",
83+
addresses: []gwv1.GatewaySpecAddress{
84+
{Type: ptr.To(gwv1.IPAddressType), Value: "2001:db8::1"},
85+
},
86+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
87+
wantIP: ptr.To("2001:db8::1"),
88+
wantErr: nil,
89+
},
90+
{
91+
name: "nil address type defaults to IPAddressType",
92+
addresses: []gwv1.GatewaySpecAddress{
93+
{Type: nil, Value: "192.0.2.1"},
94+
},
95+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
96+
wantIP: ptr.To("192.0.2.1"),
97+
wantErr: nil,
98+
},
99+
{
100+
name: "empty addresses array with LoadBalancer service",
101+
addresses: []gwv1.GatewaySpecAddress{},
102+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
103+
wantIP: nil,
104+
wantErr: nil,
105+
},
106+
{
107+
name: "multiple valid IP addresses returns error",
108+
addresses: []gwv1.GatewaySpecAddress{
109+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
110+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.11"},
111+
},
112+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
113+
wantIP: nil,
114+
wantErr: ErrMultipleAddresses,
115+
},
116+
{
117+
name: "multiple addresses with mixed types returns error",
118+
addresses: []gwv1.GatewaySpecAddress{
119+
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
120+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
121+
},
122+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
123+
wantIP: nil,
124+
wantErr: ErrMultipleAddresses,
125+
},
126+
{
127+
name: "single hostname address returns error",
128+
addresses: []gwv1.GatewaySpecAddress{
129+
{Type: ptr.To(gwv1.HostnameAddressType), Value: "example.com"},
130+
},
131+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
132+
wantIP: nil,
133+
wantErr: ErrNoValidIPAddress,
134+
},
135+
{
136+
name: "single invalid IP address returns error",
137+
addresses: []gwv1.GatewaySpecAddress{
138+
{Type: ptr.To(gwv1.IPAddressType), Value: "not-an-ip"},
139+
},
140+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
141+
wantIP: nil,
142+
wantErr: ErrNoValidIPAddress,
143+
},
144+
{
145+
name: "single invalid IP address format returns error",
146+
addresses: []gwv1.GatewaySpecAddress{
147+
{Type: ptr.To(gwv1.IPAddressType), Value: "256.256.256.256"},
148+
},
149+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
150+
wantIP: nil,
151+
wantErr: ErrNoValidIPAddress,
152+
},
153+
{
154+
name: "nil type with valid IP returns IP",
155+
addresses: []gwv1.GatewaySpecAddress{
156+
{Type: nil, Value: "203.0.113.10"},
157+
},
158+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
159+
wantIP: ptr.To("203.0.113.10"),
160+
wantErr: nil,
161+
},
162+
{
163+
name: "nil type with invalid IP returns error",
164+
addresses: []gwv1.GatewaySpecAddress{
165+
{Type: nil, Value: "invalid"},
166+
},
167+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
168+
wantIP: nil,
169+
wantErr: ErrNoValidIPAddress,
170+
},
171+
{
172+
name: "three addresses returns error",
173+
addresses: []gwv1.GatewaySpecAddress{
174+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
175+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.11"},
176+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.12"},
177+
},
178+
serviceType: ptr.To(string(corev1.ServiceTypeLoadBalancer)),
179+
wantIP: nil,
180+
wantErr: ErrMultipleAddresses,
181+
},
182+
{
183+
name: "valid IP with ClusterIP service does not set IP",
184+
addresses: []gwv1.GatewaySpecAddress{
185+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
186+
},
187+
serviceType: ptr.To(string(corev1.ServiceTypeClusterIP)),
188+
wantIP: nil,
189+
wantErr: nil,
190+
},
191+
{
192+
name: "valid IP with nil service type does not set IP",
193+
addresses: []gwv1.GatewaySpecAddress{
194+
{Type: ptr.To(gwv1.IPAddressType), Value: "203.0.113.10"},
195+
},
196+
serviceType: nil,
197+
wantIP: nil,
198+
wantErr: nil,
199+
},
200+
}
201+
202+
for _, tt := range tests {
203+
t.Run(tt.name, func(t *testing.T) {
204+
gw := &gwv1.Gateway{
205+
ObjectMeta: metav1.ObjectMeta{
206+
Name: "test-gateway",
207+
Namespace: "default",
208+
},
209+
Spec: gwv1.GatewaySpec{
210+
Addresses: tt.addresses,
211+
},
212+
}
213+
214+
svc := &HelmService{
215+
Type: tt.serviceType,
216+
}
217+
218+
err := SetLoadBalancerIPFromGateway(gw, svc)
219+
if tt.wantErr != nil {
220+
assert.Error(t, err, "expected error")
221+
assert.ErrorIs(t, err, tt.wantErr, "error type mismatch")
222+
assert.Nil(t, svc.LoadBalancerIP, "expected nil IP when error occurs")
223+
} else {
224+
assert.NoError(t, err, "unexpected error")
225+
if tt.wantIP == nil {
226+
assert.Nil(t, svc.LoadBalancerIP, "expected nil but got %v", svc.LoadBalancerIP)
227+
} else {
228+
assert.NotNil(t, svc.LoadBalancerIP, "expected non-nil IP")
229+
assert.Equal(t, *tt.wantIP, *svc.LoadBalancerIP, "IP address mismatch")
230+
}
231+
}
232+
})
233+
}
234+
}

pkg/kgateway/deployer/gateway_parameters.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ func (k *kgatewayParameters) getValues(gw *gwv1.Gateway, gwParam *kgateway.Gatew
478478

479479
// service values
480480
gateway.Service = deployer.GetServiceValues(svcConfig)
481+
// Extract loadBalancerIP from Gateway.spec.addresses and set it on the service if service type is LoadBalancer
482+
if err := deployer.SetLoadBalancerIPFromGateway(gw, gateway.Service); err != nil {
483+
return nil, err
484+
}
481485
// serviceaccount values
482486
gateway.ServiceAccount = deployer.GetServiceAccountValues(svcAccountConfig)
483487
// pod template values

pkg/kgateway/helm/agentgateway/templates/service.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ spec:
2121
{{- with $gateway.service.clusterIP }}
2222
clusterIP: {{ . }}
2323
{{- end }}
24+
{{- with $gateway.service.loadBalancerIP }}
25+
loadBalancerIP: {{ . }}
26+
{{- end }}
2427
ports:
2528
{{- range $p := $gateway.ports }}
2629
- name: {{ $p.name }}

pkg/kgateway/helm/envoy/templates/service.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ spec:
2121
{{- with $gateway.service.clusterIP }}
2222
clusterIP: {{ . }}
2323
{{- end }}
24+
{{- with $gateway.service.loadBalancerIP }}
25+
loadBalancerIP: {{ . }}
26+
{{- end }}
2427
ports:
2528
{{- range $p := $gateway.ports }}
2629
- name: {{ $p.name }}

test/deployer/internal_helm_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBtestcertdata
121121
Name: "envoy-infrastructure",
122122
InputFile: "envoy-infrastructure",
123123
},
124+
{
125+
Name: "gateway with static IP address",
126+
InputFile: "loadbalancer-static-ip",
127+
},
124128
{
125129
Name: "agentgateway-params-primary",
126130
InputFile: "agentgateway-params-primary",

0 commit comments

Comments
 (0)