Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/controller/state/graph/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,9 @@ func checkForRouteOverlap(route *L7Route, hostPortPaths map[string]string) *cond
for _, parentRef := range route.ParentRefs {
if parentRef.Attachment != nil {
port := parentRef.Attachment.ListenerPort
// FIXME(sarthyparty): https://github.com/nginx/nginx-gateway-fabric/issues/3811
// Need to merge listener hostnames with route hostnames so wildcards are handled correctly
// Also the AcceptedHostnames is a map of slices of strings, so we need to flatten it
for _, hostname := range parentRef.Attachment.AcceptedHostnames {
for _, rule := range route.Spec.Rules {
for _, match := range rule.Matches {
Expand Down
94 changes: 59 additions & 35 deletions internal/controller/state/graph/route_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type ParentRefAttachmentStatus struct {
// still attach. The backendRef condition would be displayed here.
FailedConditions []conditions.Condition
// ListenerPort is the port on the Listener that the Route is attached to.
// FIXME(sarthyparty): https://github.com/nginx/nginx-gateway-fabric/issues/3811
// Needs to be a map of <gatewayNamespacedName/listenerName> to port number
ListenerPort v1.PortNumber
// Attached indicates if the ParentRef is attached to the Gateway.
Attached bool
Expand Down Expand Up @@ -307,24 +309,37 @@ func buildSectionNameRefs(
gwNsName: gwNsName,
}

// If there is no section name, we create ParentRefs for each listener in the gateway
// If there is no section name, handle based on whether port is specified
// FIXME(sarthyparty): https://github.com/nginx/nginx-gateway-fabric/issues/3811
// this logic seems to be duplicated in findAttachableListeners so we should refactor this,
// either here or in findAttachableListeners
if p.SectionName == nil {
for _, l := range gw.Listeners {
k.sectionName = string(l.Source.Name)

if err := checkUniqueSections(k); err != nil {
return nil, err
}

// If port is specified, preserve the port-only nature for proper validation
if p.Port != nil {
sectionNameRefs = append(sectionNameRefs, ParentRef{
// if the ParentRefs we create are for each listener in the same gateway, we keep the
// parentRefIndex the same so when we look at a route's parentRef's we can see
// if the parentRef is a unique parentRef or one we created internally
Idx: i,
Gateway: CreateParentRefGateway(gw),
SectionName: &l.Source.Name,
SectionName: nil, // Keep as nil to preserve port-only semantics
Port: p.Port,
})
} else {
// If no port and no sectionName, expand to all listeners
for _, l := range gw.Listeners {
k.sectionName = string(l.Source.Name)

if err := checkUniqueSections(k); err != nil {
return nil, err
}

sectionNameRefs = append(sectionNameRefs, ParentRef{
// if the ParentRefs we create are for each listener in the same gateway, we keep the
// parentRefIndex the same so when we look at a route's parentRef's we can see
// if the parentRef is a unique parentRef or one we created internally
Idx: i,
Gateway: CreateParentRefGateway(gw),
SectionName: &l.Source.Name,
})
}
}

continue
Expand Down Expand Up @@ -532,10 +547,8 @@ func validateParentRef(

ref.Attachment = attachment

path := field.NewPath("spec").Child("parentRefs").Index(ref.Idx)

attachableListeners, listenerExists := findAttachableListeners(
getSectionName(ref.SectionName),
ref,
gw.Listeners,
)

Expand All @@ -546,17 +559,7 @@ func validateParentRef(
return attachment, nil
}

// Case 2: Attachment is not possible due to unsupported configuration.

if ref.Port != nil {
valErr := field.Forbidden(path.Child("port"), "cannot be set")
attachment.FailedConditions = append(
attachment.FailedConditions, conditions.NewRouteUnsupportedValue(valErr.Error()),
)
return attachment, attachableListeners
}

// Case 3: Attachment is not possible because Gateway is invalid
// Case 2: Attachment is not possible because Gateway is invalid

if !gw.Valid {
attachment.FailedConditions = append(attachment.FailedConditions, conditions.NewRouteInvalidGateway())
Expand Down Expand Up @@ -873,29 +876,50 @@ func tryToAttachL7RouteToListeners(

// findAttachableListeners returns a list of attachable listeners and whether the listener exists for a non-empty
// sectionName.
func findAttachableListeners(sectionName string, listeners []*Listener) ([]*Listener, bool) {
func findAttachableListeners(ref *ParentRef, listeners []*Listener) ([]*Listener, bool) {
sectionName := getSectionName(ref.SectionName)

// Case 1: sectionName is specified - look for that specific listener
if sectionName != "" {
for _, l := range listeners {
if l.Name == sectionName {
if l.Attachable {
if l.Attachable && (ref.Port == nil || l.Source.Port == *ref.Port) {
return []*Listener{l}, true
}
// We return false because we didn't find a listener that matches the port
if ref.Port != nil && l.Source.Port != *ref.Port {
return nil, false
}
// Return true because we found a listener that matches the sectionName and port but is not attachable
return nil, true
}
}
return nil, false
}

attachableListeners := make([]*Listener, 0, len(listeners))
for _, l := range listeners {
if !l.Attachable {
continue
// Case 2: Only port is specified - find all attachable listeners matching that port
if ref.Port != nil {
var attachableListeners []*Listener
var foundListener bool
for _, l := range listeners {
if l.Source.Port == *ref.Port {
foundListener = true
if l.Attachable {
attachableListeners = append(attachableListeners, l)
}
}
}

attachableListeners = append(attachableListeners, l)
return attachableListeners, foundListener
}

return attachableListeners, true
// Case 3: Neither sectionName nor port specified - return all attachable listeners
var attachableListeners []*Listener
for _, l := range listeners {
if l.Attachable {
attachableListeners = append(attachableListeners, l)
}
}
return attachableListeners, len(listeners) > 0
}

func findAcceptedHostnames(listenerHostname *v1.Hostname, routeHostnames []v1.Hostname) []string {
Expand Down
170 changes: 157 additions & 13 deletions internal/controller/state/graph/route_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,9 @@ func TestBindRouteToListeners(t *testing.T) {
Source: gw,
Valid: true,
Listeners: []*Listener{
createListener("listener-80-1"),
createModifiedListener("listener-80-1", func(l *Listener) {
l.Source.Port = 80
}),
},
},
expectedSectionNameRefs: []ParentRef{
Expand All @@ -729,19 +731,24 @@ func TestBindRouteToListeners(t *testing.T) {
Gateway: &ParentRefGateway{NamespacedName: client.ObjectKeyFromObject(gw)},
SectionName: hrWithPort.Spec.ParentRefs[0].SectionName,
Attachment: &ParentRefAttachmentStatus{
Attached: false,
FailedConditions: []conditions.Condition{
conditions.NewRouteUnsupportedValue(
`spec.parentRefs[0].port: Forbidden: cannot be set`,
),
Attached: true,
FailedConditions: nil,
AcceptedHostnames: map[string][]string{
"test/gateway/listener-80-1": {"foo.example.com"},
},
AcceptedHostnames: map[string][]string{},
ListenerPort: 80,
},
Port: hrWithPort.Spec.ParentRefs[0].Port,
},
},
expectedGatewayListeners: []*Listener{
createListener("listener-80-1"),
func() *Listener {
l := createModifiedListener("listener-80-1", func(l *Listener) {
l.Source.Port = 80
})
l.Routes[CreateRouteKey(hrWithPort)] = routeWithPort
return l
}(),
},
name: "port is configured",
},
Expand Down Expand Up @@ -1904,17 +1911,17 @@ func TestBindL4RouteToListeners(t *testing.T) {
Name: "gateway",
},
Listeners: []*Listener{
createListener("listener-443"),
createModifiedListener("listener-443", func(l *Listener) {
l.Source.Port = 443
}),
},
},
expectedSectionNameRefs: []ParentRef{
{
Attachment: &ParentRefAttachmentStatus{
AcceptedHostnames: map[string][]string{},
FailedConditions: []conditions.Condition{
conditions.NewRouteUnsupportedValue(
`spec.parentRefs[0].port: Forbidden: cannot be set`,
),
conditions.NewRouteNoMatchingParent(),
},
Attached: false,
},
Expand All @@ -1925,7 +1932,9 @@ func TestBindL4RouteToListeners(t *testing.T) {
},
},
expectedGatewayListeners: []*Listener{
createListener("listener-443"),
createModifiedListener("listener-443", func(l *Listener) {
l.Source.Port = 443
}),
},
name: "port is not nil",
},
Expand Down Expand Up @@ -3720,3 +3729,138 @@ func TestBindRoutesToListeners(t *testing.T) {
bindRoutesToListeners(nil, nil, nil, nil)
}).ToNot(Panic())
}

func TestFindAttachableListenersWithPort(t *testing.T) {
t.Parallel()

port80 := gatewayv1.PortNumber(80)
port443 := gatewayv1.PortNumber(443)
port8080 := gatewayv1.PortNumber(8080)

listeners := []*Listener{
{
Name: "http-80",
Attachable: true,
Source: gatewayv1.Listener{
Name: "http-80",
Port: port80,
},
},
{
Name: "https-443",
Attachable: true,
Source: gatewayv1.Listener{
Name: "https-443",
Port: port443,
},
},
{
Name: "http-8080",
Attachable: false, // not attachable
Source: gatewayv1.Listener{
Name: "http-8080",
Port: port8080,
},
},
}

tests := []struct {
name string
parentRef *ParentRef
expectedListeners []*Listener
expectedListenerExists bool
}{
{
name: "port 80 filter returns only port 80 listener",
parentRef: &ParentRef{
Port: &port80,
},
expectedListeners: []*Listener{listeners[0]}, // only http-80
expectedListenerExists: true,
},
{
name: "port 443 filter returns only port 443 listener",
parentRef: &ParentRef{
Port: &port443,
},
expectedListeners: []*Listener{listeners[1]}, // only https-443
expectedListenerExists: true,
},
{
name: "port 8080 filter returns empty because listener is not attachable",
parentRef: &ParentRef{
Port: &port8080,
},
expectedListeners: []*Listener{},
expectedListenerExists: true,
},
{
name: "port 9999 filter returns empty because no listener has that port",
parentRef: &ParentRef{
Port: helpers.GetPointer(gatewayv1.PortNumber(9999)),
},
expectedListeners: []*Listener{},
expectedListenerExists: false,
},
{
name: "no port specified returns all attachable listeners",
parentRef: &ParentRef{
Port: nil,
},
expectedListeners: []*Listener{listeners[0], listeners[1]}, // both attachable listeners
expectedListenerExists: true,
},
{
name: "sectionName with matching port returns that specific listener",
parentRef: &ParentRef{
SectionName: helpers.GetPointer(gatewayv1.SectionName("http-80")),
Port: &port80,
},
expectedListeners: []*Listener{listeners[0]},
expectedListenerExists: true,
},
{
name: "sectionName with non-matching port returns empty",
parentRef: &ParentRef{
SectionName: helpers.GetPointer(gatewayv1.SectionName("http-80")),
Port: &port443, // wrong port for http-80 listener
},
expectedListeners: []*Listener{},
expectedListenerExists: false,
},
{
name: "sectionName that doesn't exist returns empty with false",
parentRef: &ParentRef{
SectionName: helpers.GetPointer(gatewayv1.SectionName("nonexistent")),
Port: &port80,
},
expectedListeners: []*Listener{},
expectedListenerExists: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g := NewWithT(t)

attachableListeners, listenerExists := findAttachableListeners(tt.parentRef, listeners)

g.Expect(listenerExists).To(Equal(tt.expectedListenerExists))
g.Expect(attachableListeners).To(HaveLen(len(tt.expectedListeners)))

// Compare listeners by name since they're the same instances
expectedNames := make([]string, len(tt.expectedListeners))
for i, l := range tt.expectedListeners {
expectedNames[i] = l.Name
}

actualNames := make([]string, len(attachableListeners))
for i, l := range attachableListeners {
actualNames[i] = l.Name
}

g.Expect(actualNames).To(ConsistOf(expectedNames))
})
}
}
2 changes: 1 addition & 1 deletion tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ GW_SERVICE_TYPE = NodePort## Service type to use for the gateway
NGF_VERSION ?= edge## NGF version to be tested
PULL_POLICY = Never## Pull policy for the images
NGINX_CONF_DIR = internal/controller/nginx/conf
SUPPORTED_EXTENDED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,HTTPRoutePathRedirect,GatewayHTTPListenerIsolation,GatewayInfrastructurePropagation,HTTPRouteRequestMirror,HTTPRouteRequestMultipleMirrors,HTTPRouteRequestPercentageMirror,HTTPRouteBackendProtocolWebSocket
SUPPORTED_EXTENDED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,HTTPRoutePathRedirect,GatewayHTTPListenerIsolation,GatewayInfrastructurePropagation,HTTPRouteRequestMirror,HTTPRouteRequestMultipleMirrors,HTTPRouteRequestPercentageMirror,HTTPRouteBackendProtocolWebSocket,HTTPRouteParentRefPort,HTTPRouteDestinationPortMatching
STANDARD_CONFORMANCE_PROFILES = GATEWAY-HTTP,GATEWAY-GRPC
EXPERIMENTAL_CONFORMANCE_PROFILES = GATEWAY-TLS
CONFORMANCE_PROFILES = $(STANDARD_CONFORMANCE_PROFILES) # by default we use the standard conformance profiles. If experimental is enabled we override this and add the experimental profiles.
Expand Down
Loading