diff --git a/internal/mode/static/state/graph/route_common.go b/internal/mode/static/state/graph/route_common.go index 2db3e9e0b4..31a7c1528f 100644 --- a/internal/mode/static/state/graph/route_common.go +++ b/internal/mode/static/state/graph/route_common.go @@ -337,22 +337,96 @@ func bindRoutesToListeners( bindL7RouteToListeners(r, gw, namespaces) } - var routes []*L4Route - for _, r := range l4Routes { + routes := make([]*L7Route, 0, len(l7Routes)) + for _, r := range l7Routes { routes = append(routes, r) } + isolateL7RouteListeners(routes, gw.Listeners) + + l4RouteSlice := make([]*L4Route, 0, len(l4Routes)) + for _, r := range l4Routes { + l4RouteSlice = append(l4RouteSlice, r) + } + // Sort the slice by timestamp and name so that we process the routes in the priority order - sort.Slice(routes, func(i, j int) bool { - return ngfSort.LessClientObject(routes[i].Source, routes[j].Source) + sort.Slice(l4RouteSlice, func(i, j int) bool { + return ngfSort.LessClientObject(l4RouteSlice[i].Source, l4RouteSlice[j].Source) }) // portHostnamesMap exists to detect duplicate hostnames on the same port portHostnamesMap := make(map[string]struct{}) - for _, r := range routes { + for _, r := range l4RouteSlice { bindL4RouteToListeners(r, gw, namespaces, portHostnamesMap) } + + isolateL4RouteListeners(l4RouteSlice, gw.Listeners) +} + +// isolateL7RouteListeners ensures listener isolation for all L7Routes. +func isolateL7RouteListeners(routes []*L7Route, listeners []*Listener) { + listenerHostnameMap := make(map[string]string, len(listeners)) + for _, l := range listeners { + listenerHostnameMap[l.Name] = getHostname(l.Source.Hostname) + } + + for _, route := range routes { + isolateHostnamesForParentRefs(route.ParentRefs, listenerHostnameMap) + } +} + +// isolateL4RouteListeners ensures listener isolation for all L4Routes. +func isolateL4RouteListeners(routes []*L4Route, listeners []*Listener) { + listenerHostnameMap := make(map[string]string, len(listeners)) + for _, l := range listeners { + listenerHostnameMap[l.Name] = getHostname(l.Source.Hostname) + } + + for _, route := range routes { + isolateHostnamesForParentRefs(route.ParentRefs, listenerHostnameMap) + } +} + +// isolateHostnamesForParentRefs iterates through the parentRefs of a route to identify the list of accepted hostnames +// for each listener. If any accepted hostname belongs to another listener, +// it removes those hostnames to ensure listener isolation. +func isolateHostnamesForParentRefs(parentRef []ParentRef, listenerHostnameMap map[string]string) { + for _, ref := range parentRef { + acceptedHostnames := ref.Attachment.AcceptedHostnames + + hostnamesToRemoves := make(map[string]struct{}) + for listenerName, hostnames := range acceptedHostnames { + if len(hostnames) == 0 { + continue + } + for _, h := range hostnames { + for lName, lHostname := range listenerHostnameMap { + // skip comparison if it is a catch all listener block + if lHostname == "" { + continue + } + if h == lHostname && listenerName != lName { + hostnamesToRemoves[h] = struct{}{} + } + } + } + + isolatedHostnames := removeHostnames(hostnames, hostnamesToRemoves) + ref.Attachment.AcceptedHostnames[listenerName] = isolatedHostnames + } + } +} + +// removeHostnames removes the hostnames that are part of toRemove slice. +func removeHostnames(hostnames []string, toRemove map[string]struct{}) []string { + result := make([]string, 0, len(hostnames)) + for _, hostname := range hostnames { + if _, exists := toRemove[hostname]; !exists { + result = append(result, hostname) + } + } + return result } func validateParentRef( diff --git a/internal/mode/static/state/graph/route_common_test.go b/internal/mode/static/state/graph/route_common_test.go index 42f601e5df..32df93de4a 100644 --- a/internal/mode/static/state/graph/route_common_test.go +++ b/internal/mode/static/state/graph/route_common_test.go @@ -2175,3 +2175,551 @@ func TestTryToAttachL4RouteToListeners_NoAttachableListeners(t *testing.T) { g.Expect(cond).To(Equal(staticConds.NewRouteInvalidListener())) g.Expect(attachable).To(BeFalse()) } + +func TestIsolateL4Listeners(t *testing.T) { + t.Parallel() + gw := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gateway", + }, + } + + createTLSRouteWithSectionNameAndPort := func( + name string, + sectionName *gatewayv1.SectionName, + ns string, + hostnames ...gatewayv1.Hostname, + ) *v1alpha2.TLSRoute { + return &v1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + Spec: v1alpha2.TLSRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Name: gatewayv1.ObjectName(gw.Name), + SectionName: sectionName, + }, + }, + }, + Hostnames: hostnames, + }, + } + } + + routeHostnames := []gatewayv1.Hostname{"bar.com", "*.example.com", "*.foo.example.com", "abc.foo.example.com"} + tr1 := createTLSRouteWithSectionNameAndPort( + "tr1", + helpers.GetPointer[gatewayv1.SectionName]("empty-hostname"), + "test", + routeHostnames..., + ) + tr2 := createTLSRouteWithSectionNameAndPort( + "tr2", + helpers.GetPointer[gatewayv1.SectionName]("wildcard-example-com"), + "test", + routeHostnames..., + ) + tr3 := createTLSRouteWithSectionNameAndPort( + "tr3", + helpers.GetPointer[gatewayv1.SectionName]("foo-wildcard-example-com"), + "test", + routeHostnames..., + ) + tr4 := createTLSRouteWithSectionNameAndPort( + "tr4", + helpers.GetPointer[gatewayv1.SectionName]("abc-com"), + "test", + routeHostnames..., + ) + tr5 := createTLSRouteWithSectionNameAndPort( + "tr5", + helpers.GetPointer[gatewayv1.SectionName]("no-match"), + "test", + routeHostnames..., + ) + + createL4RoutewithAcceptedHostnames := func( + source *v1alpha2.TLSRoute, + acceptedHostnames map[string][]string, + hostnames []gatewayv1.Hostname, + sectionName *gatewayv1.SectionName, + ) *L4Route { + return &L4Route{ + Source: source, + Spec: L4RouteSpec{ + Hostnames: hostnames, + }, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKey{ + Namespace: gw.Namespace, + Name: gw.Name, + }, + SectionName: sectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: acceptedHostnames, + Attached: true, + }, + }, + }, + } + } + + createListener := func(name string, hostname string) *Listener { + return &Listener{ + Name: name, + Source: gatewayv1.Listener{ + Name: gatewayv1.SectionName(name), + Hostname: (*gatewayv1.Hostname)(helpers.GetPointer(hostname)), + }, + Valid: true, + Attachable: true, + } + } + + acceptedHostnamesEmptyHostname := map[string][]string{ + "empty-hostname": { + "bar.com", "*.example.com", "*.foo.example.com", "abc.foo.example.com", + }, + } + acceptedHostnamesWildcardExample := map[string][]string{ + "wildcard-example-com": { + "*.example.com", "*.foo.example.com", "abc.foo.example.com", + }, + } + + acceptedHostnamesFooWildcardExample := map[string][]string{ + "foo-wildcard-example-com": { + "*.foo.example.com", "abc.foo.example.com", + }, + } + + acceptedHostnamesAbcCom := map[string][]string{ + "abc-com": { + "abc.foo.example.com", + }, + } + acceptedHostnamesNoMatch := map[string][]string{ + "no-match": {}, + } + + routes := []*L4Route{ + createL4RoutewithAcceptedHostnames( + tr1, acceptedHostnamesEmptyHostname, + []gatewayv1.Hostname{"bar.com"}, + helpers.GetPointer[gatewayv1.SectionName]("empty-hostname"), + ), + createL4RoutewithAcceptedHostnames( + tr2, + acceptedHostnamesWildcardExample, + []gatewayv1.Hostname{"*.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("wildcard-example-com"), + ), + createL4RoutewithAcceptedHostnames( + tr3, + acceptedHostnamesFooWildcardExample, + []gatewayv1.Hostname{"*.foo.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("foo-wildcard-example-com"), + ), + createL4RoutewithAcceptedHostnames( + tr4, + acceptedHostnamesAbcCom, + []gatewayv1.Hostname{"abc.foo.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("abc-com"), + ), + createL4RoutewithAcceptedHostnames( + tr5, + acceptedHostnamesNoMatch, + []gatewayv1.Hostname{"cafe.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("no-match"), + ), + } + + listeners := []*Listener{ + createListener("empty-hostname", ""), + createListener("wildcard-example-com", "*.example.com"), + createListener("foo-wildcard-example-com", "*.foo.example.com"), + createListener("abc-com", "abc.foo.example.com"), + createListener("no-match", "no-match.cafe.com"), + } + + expectedResult := map[string][]ParentRef{ + "tr1": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: tr1.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "empty-hostname": {"bar.com"}, + }, + Attached: true, + }, + }, + }, + "tr2": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: tr2.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "wildcard-example-com": {"*.example.com"}, + }, + Attached: true, + }, + }, + }, + "tr3": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: tr3.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "foo-wildcard-example-com": {"*.foo.example.com"}, + }, + Attached: true, + }, + }, + }, + "tr4": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: tr4.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "abc-com": {"abc.foo.example.com"}, + }, + Attached: true, + }, + }, + }, + "tr5": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: tr5.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "no-match": {}, + }, + Attached: true, + }, + }, + }, + } + + g := NewWithT(t) + isolateL4RouteListeners(routes, listeners) + + result := map[string][]ParentRef{} + for _, route := range routes { + result[route.Source.GetName()] = route.ParentRefs + } + g.Expect(helpers.Diff(result, expectedResult)).To(BeEmpty()) +} + +func TestIsolateL7Listeners(t *testing.T) { + t.Parallel() + gw := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gateway", + }, + } + + createHTTPRouteWithSectionNameAndPort := func( + name string, + sectionName *gatewayv1.SectionName, + ns string, + hostnames ...gatewayv1.Hostname, + ) *gatewayv1.HTTPRoute { + return &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Name: gatewayv1.ObjectName(gw.Name), + SectionName: sectionName, + }, + }, + }, + Hostnames: hostnames, + }, + } + } + + routeHostnames := []gatewayv1.Hostname{"bar.com", "*.example.com", "*.foo.example.com", "abc.foo.example.com"} + hr1 := createHTTPRouteWithSectionNameAndPort( + "hr1", + helpers.GetPointer[gatewayv1.SectionName]("empty-hostname"), + "test", + routeHostnames..., + ) + hr2 := createHTTPRouteWithSectionNameAndPort( + "hr2", + helpers.GetPointer[gatewayv1.SectionName]("wildcard-example-com"), + "test", + routeHostnames..., + ) + hr3 := createHTTPRouteWithSectionNameAndPort( + "hr3", + helpers.GetPointer[gatewayv1.SectionName]("foo-wildcard-example-com"), + "test", + routeHostnames..., + ) + hr4 := createHTTPRouteWithSectionNameAndPort( + "hr4", + helpers.GetPointer[gatewayv1.SectionName]("abc-com"), + "test", + routeHostnames..., + ) + hr5 := createHTTPRouteWithSectionNameAndPort( + "hr5", + helpers.GetPointer[gatewayv1.SectionName]("no-match"), + "test", + routeHostnames..., // no matching hostname + ) + + createL7RoutewithAcceptedHostnames := func( + source *gatewayv1.HTTPRoute, + acceptedHostnames map[string][]string, + hostnames []gatewayv1.Hostname, + sectionName *gatewayv1.SectionName, + ) *L7Route { + return &L7Route{ + Source: source, + Spec: L7RouteSpec{ + Hostnames: hostnames, + }, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKey{ + Namespace: gw.Namespace, + Name: gw.Name, + }, + SectionName: sectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: acceptedHostnames, + Attached: true, + }, + }, + }, + } + } + + createListener := func(name string, hostname string) *Listener { + return &Listener{ + Name: name, + Source: gatewayv1.Listener{ + Name: gatewayv1.SectionName(name), + Hostname: (*gatewayv1.Hostname)(helpers.GetPointer(hostname)), + }, + Valid: true, + Attachable: true, + } + } + + acceptedHostnamesEmptyHostname := map[string][]string{ + "empty-hostname": { + "bar.com", "*.example.com", "*.foo.example.com", "abc.foo.example.com", + }, + } + acceptedHostnamesWildcardExample := map[string][]string{ + "wildcard-example-com": { + "*.example.com", "*.foo.example.com", "abc.foo.example.com", + }, + } + + acceptedHostnamesFooWildcardExample := map[string][]string{ + "foo-wildcard-example-com": { + "*.foo.example.com", "abc.foo.example.com", + }, + } + + acceptedHostnamesAbcCom := map[string][]string{ + "abc-com": { + "abc.foo.example.com", + }, + } + acceptedHostnamesNoMatch := map[string][]string{ + "no-match": {}, + } + + routes := []*L7Route{ + createL7RoutewithAcceptedHostnames( + hr1, + acceptedHostnamesEmptyHostname, + []gatewayv1.Hostname{"bar.com"}, + helpers.GetPointer[gatewayv1.SectionName]("empty-hostname"), + ), + createL7RoutewithAcceptedHostnames( + hr2, + acceptedHostnamesWildcardExample, + []gatewayv1.Hostname{"*.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("wildcard-example-com"), + ), + createL7RoutewithAcceptedHostnames( + hr3, + acceptedHostnamesFooWildcardExample, + []gatewayv1.Hostname{"*.foo.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("foo-wildcard-example-com"), + ), + createL7RoutewithAcceptedHostnames( + hr4, + acceptedHostnamesAbcCom, + []gatewayv1.Hostname{"abc.foo.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("abc-com"), + ), + createL7RoutewithAcceptedHostnames( + hr5, + acceptedHostnamesNoMatch, + []gatewayv1.Hostname{"cafe.example.com"}, + helpers.GetPointer[gatewayv1.SectionName]("no-match"), + ), + } + + listeners := []*Listener{ + createListener("empty-hostname", ""), + createListener("wildcard-example-com", "*.example.com"), + createListener("foo-wildcard-example-com", "*.foo.example.com"), + createListener("abc-com", "abc.foo.example.com"), + createListener("no-match", "no-match.cafe.com"), + } + + expectedResult := map[string][]ParentRef{ + "hr1": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr1.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "empty-hostname": {"bar.com"}, + }, + Attached: true, + }, + }, + }, + "hr2": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr2.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "wildcard-example-com": {"*.example.com"}, + }, + Attached: true, + }, + }, + }, + "hr3": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr3.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "foo-wildcard-example-com": {"*.foo.example.com"}, + }, + Attached: true, + }, + }, + }, + "hr4": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr4.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "abc-com": {"abc.foo.example.com"}, + }, + Attached: true, + }, + }, + }, + "hr5": { + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr5.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + AcceptedHostnames: map[string][]string{ + "no-match": {}, + }, + Attached: true, + }, + }, + }, + } + + g := NewWithT(t) + isolateL7RouteListeners(routes, listeners) + + result := map[string][]ParentRef{} + for _, route := range routes { + result[route.Source.GetName()] = route.ParentRefs + } + + g.Expect(helpers.Diff(result, expectedResult)).To(BeEmpty()) +} + +func TestRemoveHostnames(t *testing.T) { + t.Parallel() + tests := []struct { + name string + hostnames []string + removeHostnames map[string]struct{} + expectedHostnames []string + }{ + { + name: "remove multiple hostnames", + hostnames: []string{"foo.example.com", "bar.example.com", "bar.com", "*.wildcard.com"}, + removeHostnames: map[string]struct{}{ + "foo.example.com": {}, + "bar.example.com": {}, + }, + expectedHostnames: []string{"bar.com", "*.wildcard.com"}, + }, + { + name: "remove all hostnames", + hostnames: []string{"foo.example.com", "bar.example.com", "bar.com", "*.wildcard.com"}, + removeHostnames: map[string]struct{}{ + "foo.example.com": {}, + "bar.example.com": {}, + "bar.com": {}, + "*.wildcard.com": {}, + }, + expectedHostnames: []string{}, + }, + { + name: "remove no hostnames", + hostnames: []string{"foo.example.com", "bar.example.com", "bar.com", "*.wildcard.com"}, + removeHostnames: map[string]struct{}{}, + expectedHostnames: []string{"foo.example.com", "bar.example.com", "bar.com", "*.wildcard.com"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + result := removeHostnames(tt.hostnames, tt.removeHostnames) + g.Expect(result).To(Equal(tt.expectedHostnames)) + }) + } +} diff --git a/tests/Makefile b/tests/Makefile index 2fa1922162..c95f337d19 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -14,7 +14,7 @@ NGF_VERSION ?= edge## NGF version to be tested PULL_POLICY = Never## Pull policy for the images NGINX_CONF_DIR = internal/mode/static/nginx/conf PROVISIONER_MANIFEST = conformance/provisioner/provisioner.yaml -SUPPORTED_EXTENDED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,HTTPRouteResponseHeaderModification,HTTPRoutePathRedirect +SUPPORTED_EXTENDED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,HTTPRouteResponseHeaderModification,HTTPRoutePathRedirect,GatewayHTTPListenerIsolation 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.