Skip to content

Commit 36788a1

Browse files
authored
Add support for gateway addresses field (#3896)
Problem: Users would like a way to specify addresses for all incoming traffic to use through the GatewaySpecAddresses field. Solution: Implement the GatewaySpecAddresses field by adding the validated addresses to the externalIP field on the NGINX service. Testing: GatewayStaticAddresses conformance test passes. Manually verified service externalIP field is set correctly. Manually verified conditions on Gateway.
1 parent 24e0cb6 commit 36788a1

File tree

12 files changed

+276
-51
lines changed

12 files changed

+276
-51
lines changed

internal/controller/handler.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"net"
78
"strings"
89
"sync"
910
"time"
@@ -508,6 +509,14 @@ func getGatewayAddresses(
508509
addresses = append(addresses, gwSvc.Spec.ClusterIP)
509510
}
510511

512+
for _, address := range gateway.Source.Spec.Addresses {
513+
if address.Type != nil &&
514+
*address.Type == gatewayv1.IPAddressType &&
515+
net.ParseIP(address.Value) != nil {
516+
addresses = append(addresses, address.Value)
517+
}
518+
}
519+
511520
gwAddresses := make([]gatewayv1.GatewayStatusAddress, 0, len(addresses)+len(hostnames))
512521
for _, addr := range addresses {
513522
statusAddr := gatewayv1.GatewayStatusAddress{

internal/controller/handler_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,18 @@ var _ = Describe("getGatewayAddresses", func() {
543543
Name: "gateway",
544544
Namespace: "test",
545545
},
546+
Spec: gatewayv1.GatewaySpec{
547+
Addresses: []gatewayv1.GatewaySpecAddress{
548+
{
549+
Type: helpers.GetPointer(gatewayv1.IPAddressType),
550+
Value: "192.0.2.1",
551+
},
552+
{
553+
Type: helpers.GetPointer(gatewayv1.IPAddressType),
554+
Value: "192.0.2.3",
555+
},
556+
},
557+
},
546558
},
547559
}
548560

@@ -580,9 +592,11 @@ var _ = Describe("getGatewayAddresses", func() {
580592

581593
addrs, err = getGatewayAddresses(context.Background(), fakeClient, &svc, gateway, "nginx")
582594
Expect(err).ToNot(HaveOccurred())
583-
Expect(addrs).To(HaveLen(2))
595+
Expect(addrs).To(HaveLen(4))
584596
Expect(addrs[0].Value).To(Equal("34.35.36.37"))
585-
Expect(addrs[1].Value).To(Equal("myhost"))
597+
Expect(addrs[1].Value).To(Equal("192.0.2.1"))
598+
Expect(addrs[2].Value).To(Equal("192.0.2.3"))
599+
Expect(addrs[3].Value).To(Equal("myhost"))
586600

587601
Expect(fakeClient.Delete(context.Background(), &svc)).To(Succeed())
588602
// Create ClusterIP Service
@@ -601,8 +615,10 @@ var _ = Describe("getGatewayAddresses", func() {
601615

602616
addrs, err = getGatewayAddresses(context.Background(), fakeClient, &svc, gateway, "nginx")
603617
Expect(err).ToNot(HaveOccurred())
604-
Expect(addrs).To(HaveLen(1))
618+
Expect(addrs).To(HaveLen(3))
605619
Expect(addrs[0].Value).To(Equal("12.13.14.15"))
620+
Expect(addrs[1].Value).To(Equal("192.0.2.1"))
621+
Expect(addrs[2].Value).To(Equal("192.0.2.3"))
606622
})
607623
})
608624

internal/controller/provisioner/objects.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
167167
Annotations: maps.Clone(objectMeta.Annotations),
168168
}
169169

170-
service, err := buildNginxService(serviceObjectMeta, nProxyCfg, ports, selectorLabels)
170+
service, err := buildNginxService(serviceObjectMeta, nProxyCfg, ports, selectorLabels, gateway.Spec.Addresses)
171171
if err != nil {
172172
errs = append(errs, err)
173173
}
@@ -517,6 +517,7 @@ func buildNginxService(
517517
nProxyCfg *graph.EffectiveNginxProxy,
518518
ports map[int32]struct{},
519519
selectorLabels map[string]string,
520+
addresses []gatewayv1.GatewaySpecAddress,
520521
) (*corev1.Service, error) {
521522
var serviceCfg ngfAPIv1alpha2.ServiceSpec
522523
if nProxyCfg != nil && nProxyCfg.Kubernetes != nil && nProxyCfg.Kubernetes.Service != nil {
@@ -572,6 +573,8 @@ func buildNginxService(
572573
},
573574
}
574575

576+
setSvcExternalIPs(svc, addresses)
577+
575578
setIPFamily(nProxyCfg, svc)
576579

577580
setSvcLoadBalancerSettings(serviceCfg, &svc.Spec)
@@ -586,6 +589,14 @@ func buildNginxService(
586589
return svc, nil
587590
}
588591

592+
func setSvcExternalIPs(svc *corev1.Service, addresses []gatewayv1.GatewaySpecAddress) {
593+
for _, address := range addresses {
594+
if address.Type != nil && *address.Type == gatewayv1.IPAddressType {
595+
svc.Spec.ExternalIPs = append(svc.Spec.ExternalIPs, address.Value)
596+
}
597+
}
598+
}
599+
589600
func setIPFamily(nProxyCfg *graph.EffectiveNginxProxy, svc *corev1.Service) {
590601
if nProxyCfg != nil && nProxyCfg.IPFamily != nil && *nProxyCfg.IPFamily != ngfAPIv1alpha2.Dual {
591602
svc.Spec.IPFamilyPolicy = helpers.GetPointer(corev1.IPFamilyPolicySingleStack)

internal/controller/provisioner/objects_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ func TestBuildNginxResourceObjects(t *testing.T) {
8181
Port: 9999,
8282
},
8383
},
84+
Addresses: []gatewayv1.GatewaySpecAddress{
85+
{
86+
Type: helpers.GetPointer(gatewayv1.IPAddressType),
87+
Value: "192.0.0.2",
88+
},
89+
},
8490
},
8591
}
8692

@@ -185,6 +191,7 @@ func TestBuildNginxResourceObjects(t *testing.T) {
185191
TargetPort: intstr.FromInt(9999),
186192
},
187193
}))
194+
g.Expect(svc.Spec.ExternalIPs).To(Equal([]string{"192.0.0.2"}))
188195

189196
depObj := objects[5]
190197
dep, ok := depObj.(*appsv1.Deployment)

internal/controller/state/conditions/conditions.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -840,22 +840,36 @@ func NewGatewayInvalid(msg string) []Condition {
840840
}
841841
}
842842

843-
// NewGatewayUnsupportedValue returns Conditions that indicate that a field of the Gateway has an unsupported value.
844-
// Unsupported means that the value is not supported by the implementation or invalid.
845-
func NewGatewayUnsupportedValue(msg string) []Condition {
846-
return []Condition{
847-
{
848-
Type: string(v1.GatewayConditionAccepted),
849-
Status: metav1.ConditionFalse,
850-
Reason: string(GatewayReasonUnsupportedValue),
851-
Message: msg,
852-
},
853-
{
854-
Type: string(v1.GatewayConditionProgrammed),
855-
Status: metav1.ConditionFalse,
856-
Reason: string(GatewayReasonUnsupportedValue),
857-
Message: msg,
858-
},
843+
// NewGatewayUnsupportedAddress returns a Condition that indicates the Gateway is not accepted because it
844+
// contains an address type that is not supported.
845+
func NewGatewayUnsupportedAddress(msg string) Condition {
846+
return Condition{
847+
Type: string(v1.GatewayConditionAccepted),
848+
Status: metav1.ConditionFalse,
849+
Reason: string(v1.GatewayReasonUnsupportedAddress),
850+
Message: msg,
851+
}
852+
}
853+
854+
// NewGatewayUnusableAddress returns a Condition that indicates the Gateway is not programmed because it
855+
// contains an address type that can't be used.
856+
func NewGatewayUnusableAddress(msg string) Condition {
857+
return Condition{
858+
Type: string(v1.GatewayConditionProgrammed),
859+
Status: metav1.ConditionFalse,
860+
Reason: string(v1.GatewayReasonAddressNotUsable),
861+
Message: msg,
862+
}
863+
}
864+
865+
// NewGatewayAddressNotAssigned returns a Condition that indicates the Gateway is not programmed because it
866+
// has not assigned an address for the Gateway.
867+
func NewGatewayAddressNotAssigned(msg string) Condition {
868+
return Condition{
869+
Type: string(v1.GatewayConditionProgrammed),
870+
Status: metav1.ConditionFalse,
871+
Reason: string(v1.GatewayReasonAddressNotAssigned),
872+
Message: msg,
859873
}
860874
}
861875

internal/controller/state/graph/gateway.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,14 @@ func validateGateway(gw *v1.Gateway, gc *GatewayClass, npCfg *NginxProxy) ([]con
179179
conds = append(conds, conditions.NewGatewayInvalid("GatewayClass is invalid")...)
180180
}
181181

182-
if len(gw.Spec.Addresses) > 0 {
183-
path := field.NewPath("spec", "addresses")
184-
valErr := field.Forbidden(path, "addresses are not supported")
185-
186-
conds = append(conds, conditions.NewGatewayUnsupportedValue(valErr.Error())...)
182+
// Set the unaccepted conditions here, because those make the gateway invalid. We set the unprogrammed conditions
183+
// elsewhere, because those do not make the gateway invalid.
184+
for _, address := range gw.Spec.Addresses {
185+
if address.Type == nil {
186+
conds = append(conds, conditions.NewGatewayUnsupportedAddress("AddressType must be specified"))
187+
} else if *address.Type != v1.IPAddressType {
188+
conds = append(conds, conditions.NewGatewayUnsupportedAddress("Only AddressType IPAddress is supported"))
189+
}
187190
}
188191

189192
// we evaluate validity before validating parametersRef because an invalid parametersRef/NginxProxy does not

internal/controller/state/graph/gateway_test.go

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,30 +1120,6 @@ func TestBuildGateway(t *testing.T) {
11201120
},
11211121
name: "port/protocol collisions",
11221122
},
1123-
{
1124-
gateway: createGateway(
1125-
gatewayCfg{
1126-
name: "gateway1",
1127-
listeners: []v1.Listener{foo80Listener1, foo443HTTPSListener1},
1128-
addresses: []v1.GatewaySpecAddress{{}},
1129-
},
1130-
),
1131-
gatewayClass: validGC,
1132-
expected: map[types.NamespacedName]*Gateway{
1133-
{Namespace: "test", Name: "gateway1"}: {
1134-
Source: getLastCreatedGateway(),
1135-
DeploymentName: types.NamespacedName{
1136-
Namespace: "test",
1137-
Name: controller.CreateNginxResourceName("gateway1", gcName),
1138-
},
1139-
Valid: false,
1140-
Conditions: conditions.NewGatewayUnsupportedValue("spec." +
1141-
"addresses: Forbidden: addresses are not supported",
1142-
),
1143-
},
1144-
},
1145-
name: "gateway addresses are not supported",
1146-
},
11471123
{
11481124
gateway: nil,
11491125
expected: nil,
@@ -1484,6 +1460,59 @@ func TestBuildGateway(t *testing.T) {
14841460
},
14851461
name: "invalid gatewayclass and invalid NginxProxy",
14861462
},
1463+
{
1464+
name: "invalid gateway; gateway addresses type unspecified",
1465+
gateway: createGateway(gatewayCfg{
1466+
name: "gateway-addr-unspecified",
1467+
listeners: []v1.Listener{foo80Listener1},
1468+
addresses: []v1.GatewaySpecAddress{
1469+
{
1470+
Value: "198.0.0.1",
1471+
},
1472+
},
1473+
}),
1474+
gatewayClass: validGC,
1475+
expected: map[types.NamespacedName]*Gateway{
1476+
{Namespace: "test", Name: "gateway-addr-unspecified"}: {
1477+
Source: getLastCreatedGateway(),
1478+
DeploymentName: types.NamespacedName{
1479+
Namespace: "test",
1480+
Name: controller.CreateNginxResourceName("gateway-addr-unspecified", gcName),
1481+
},
1482+
Valid: false,
1483+
Conditions: []conditions.Condition{
1484+
conditions.NewGatewayUnsupportedAddress("AddressType must be specified"),
1485+
},
1486+
},
1487+
},
1488+
},
1489+
{
1490+
name: "invalid gateway; gateway addresses type unsupported",
1491+
gateway: createGateway(gatewayCfg{
1492+
name: "gateway-addr-unsupported",
1493+
listeners: []v1.Listener{foo80Listener1},
1494+
addresses: []v1.GatewaySpecAddress{
1495+
{
1496+
Type: helpers.GetPointer(v1.HostnameAddressType),
1497+
Value: "example.com",
1498+
},
1499+
},
1500+
}),
1501+
gatewayClass: validGC,
1502+
expected: map[types.NamespacedName]*Gateway{
1503+
{Namespace: "test", Name: "gateway-addr-unsupported"}: {
1504+
Source: getLastCreatedGateway(),
1505+
DeploymentName: types.NamespacedName{
1506+
Namespace: "test",
1507+
Name: controller.CreateNginxResourceName("gateway-addr-unsupported", gcName),
1508+
},
1509+
Valid: false,
1510+
Conditions: []conditions.Condition{
1511+
conditions.NewGatewayUnsupportedAddress("Only AddressType IPAddress is supported"),
1512+
},
1513+
},
1514+
},
1515+
},
14871516
}
14881517

14891518
secretResolver := newSecretResolver(

internal/controller/status/prepare_requests.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package status
22

33
import (
44
"fmt"
5+
"net"
6+
"reflect"
57

68
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
79
"k8s.io/apimachinery/pkg/types"
@@ -17,6 +19,10 @@ import (
1719
"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/kinds"
1820
)
1921

22+
// unusableGatewayIPAddress 198.51.100.0 is a publicly reserved IP address specifically for documentation.
23+
// This is needed to give the conformance tests an example valid ip unusable address.
24+
const unusableGatewayIPAddress = "198.51.100.0"
25+
2026
// PrepareRouteRequests prepares status UpdateRequests for the given Routes.
2127
func PrepareRouteRequests(
2228
l4routes map[graph.L4RouteKey]*graph.L4Route,
@@ -329,6 +335,20 @@ func prepareGatewayRequest(
329335
)
330336
}
331337

338+
// Set the unprogrammed conditions here, because those do not make the gateway invalid.
339+
// We set the unaccepted conditions elsewhere, because those do make the gateway invalid.
340+
for _, address := range gateway.Source.Spec.Addresses {
341+
if address.Value == "" {
342+
gwConds = append(gwConds, conditions.NewGatewayAddressNotAssigned("Dynamically assigned addresses for the "+
343+
"Gateway addresses field are not supported, value must be specified"))
344+
} else {
345+
ip := net.ParseIP(address.Value)
346+
if ip == nil || reflect.DeepEqual(ip, net.ParseIP(unusableGatewayIPAddress)) {
347+
gwConds = append(gwConds, conditions.NewGatewayUnusableAddress("Invalid IP address"))
348+
}
349+
}
350+
}
351+
332352
apiGwConds := conditions.ConvertConditions(
333353
conditions.DeduplicateConditions(gwConds),
334354
gateway.Source.Generation,

0 commit comments

Comments
 (0)