Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gateway): check hostname intersection between HTTPRoute and Gateway listener #4537

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package attachment

import (
"context"
"strings"

"github.com/pkg/errors"
kube_core "k8s.io/api/core/v1"
Expand Down Expand Up @@ -144,11 +145,35 @@ func getParentRefGateway(
return gateway, nil
}

func hostnamesIntersect(routeHostnames []gatewayapi.Hostname, listenerHostnames []gatewayapi.Hostname) bool {
if len(routeHostnames) == 0 {
return true
}

for _, routeHostname := range routeHostnames {
for _, listenerHostname := range listenerHostnames {
if listenerHostname == "" {
return true
}
if routeHostname == listenerHostname {
return true
}
// If the gateway hostname is a wildcard and the route hostname
// matches the wildcard
if suffix := strings.TrimPrefix(string(listenerHostname), "*."); len(suffix) != len(listenerHostname) {
return strings.HasSuffix(string(routeHostname), suffix)
}
}
}
return false
}

// EvaluateParentRefAttachment reports whether a route in the given namespace can attach
// via the given ParentRef.
func EvaluateParentRefAttachment(
ctx context.Context,
client kube_client.Client,
routeHostnames []gatewayapi.Hostname,
routeNs *kube_core.Namespace,
ref gatewayapi.ParentReference,
) (Attachment, error) {
Expand All @@ -160,5 +185,21 @@ func EvaluateParentRefAttachment(
return Unknown, nil
}

var listenerHostnames []gatewayapi.Hostname
for _, listener := range gateway.Spec.Listeners {
if ref.SectionName != nil && *ref.SectionName != listener.Name {
continue
}
listenerHostname := gatewayapi.Hostname("")
if listener.Hostname != nil {
listenerHostname = *listener.Hostname
}
listenerHostnames = append(listenerHostnames, listenerHostname)
}

if !hostnamesIntersect(routeHostnames, listenerHostnames) {
return Invalid, nil
}

return findRouteListenerAttachment(gateway, routeNs, ref.SectionName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ import (
"github.com/kumahq/kuma/pkg/test"
)

func TestAllowedRoutes(t *testing.T) {
test.RunSpecs(t, "Gateway API AllowedRoutes support")
func TestRouteAttachment(t *testing.T) {
test.RunSpecs(t, "Gateway API route attachment support")
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,94 @@ var _ = BeforeSuite(func() {
Expect(err).NotTo(HaveOccurred())
})

var _ = Describe("Hostname intersection support", func() {
var kubeClient kube_client.Client
var simpleRef gatewayapi.ParentReference
BeforeEach(func() {
kubeClient = kube_client_fake.NewClientBuilder().WithScheme(k8sScheme).WithObjects(
gatewayClass,
gateway,
gatewayMultipleListeners,
defaultRouteNs,
otherRouteNs,
).Build()
})

checkAttachment := func(expected attachment.Attachment) func([]gatewayapi.Hostname) {
return func(routeHostnames []gatewayapi.Hostname) {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
routeHostnames,
defaultRouteNs,
simpleRef,
)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal(expected))
}
}

Context("all listeners", func() {
BeforeEach(func() {
simpleRef = *gatewayRef.DeepCopy()
simpleRef.Name = gatewayapi.ObjectName(gatewayMultipleListeners.Name)
})

It("matches with all listeners", func() {
checkAttachment(attachment.Allowed)([]gatewayapi.Hostname{"other.local"})
})
})

Context("listener without hostname", func() {
BeforeEach(func() {
simpleRef = *gatewayRef.DeepCopy()
simpleRef.Name = gatewayapi.ObjectName(gatewayMultipleListeners.Name)
simpleRef.SectionName = &anyHostnameListenerName
})

DescribeTable("matches", checkAttachment(attachment.Allowed),
Entry("without route hostname", nil),
Entry("with some hostname", []gatewayapi.Hostname{"other.local"}),
)
})

Context("listener with simple hostname", func() {
BeforeEach(func() {
simpleRef = *gatewayRef.DeepCopy()
simpleRef.Name = gatewayapi.ObjectName(gatewayMultipleListeners.Name)
simpleRef.SectionName = &simpleHostnameListenerName
})

DescribeTable("matches", checkAttachment(attachment.Allowed),
Entry("on exact match", []gatewayapi.Hostname{"simple.local"}),
Entry("if one matches", []gatewayapi.Hostname{"other.local", "simple.local"}),
)

DescribeTable("doesn't match", checkAttachment(attachment.Invalid),
Entry("without intersection", []gatewayapi.Hostname{"other.local"}),
)
})

Context("listener with wildcard hostname", func() {
BeforeEach(func() {
simpleRef = *gatewayRef.DeepCopy()
simpleRef.Name = gatewayapi.ObjectName(gatewayMultipleListeners.Name)
simpleRef.SectionName = &wildcardListenerName
})

DescribeTable("matches", checkAttachment(attachment.Allowed),
Entry("with a complete hostname", []gatewayapi.Hostname{"something.wildcard.local"}),
Entry("with a complete hostname with extra subdomains", []gatewayapi.Hostname{"something.else.wildcard.local"}),
Entry("with a complete hostname with extra subdomain and wildcard", []gatewayapi.Hostname{"*.else.wildcard.local"}),
Entry("with a wildcard hostname", []gatewayapi.Hostname{"*.wildcard.local"}),
)

DescribeTable("doesn't match", checkAttachment(attachment.Invalid),
Entry("without intersection", []gatewayapi.Hostname{"other.local"}),
)
})
})

var _ = Describe("AllowedRoutes support", func() {
var kubeClient kube_client.Client
BeforeEach(func() {
Expand All @@ -49,6 +137,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
defaultRouteNs,
simpleRef,
)
Expand All @@ -60,6 +149,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
defaultRouteNs,
simpleRef,
)
Expand All @@ -74,6 +164,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
otherRouteNs,
simpleRef,
)
Expand All @@ -88,6 +179,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
otherRouteNs,
simpleRef,
)
Expand All @@ -108,6 +200,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
defaultRouteNs,
parentRef,
)
Expand All @@ -122,6 +215,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
otherRouteNs,
parentRef,
)
Expand All @@ -135,6 +229,7 @@ var _ = Describe("AllowedRoutes support", func() {
res, err := attachment.EvaluateParentRefAttachment(
context.Background(),
kubeClient,
nil,
otherRouteNs,
parentRef,
)
Expand All @@ -145,12 +240,14 @@ var _ = Describe("AllowedRoutes support", func() {
})

var (
defaultNs = "default"
otherNs = "other"
fromAll = gatewayapi.NamespacesFromAll
fromSame = gatewayapi.NamespacesFromSame
gatewayGroup = gatewayapi.Group(gatewayapi.GroupName)
gatewayKind = gatewayapi.Kind("Gateway")
defaultNs = "default"
otherNs = "other"
fromAll = gatewayapi.NamespacesFromAll
fromSame = gatewayapi.NamespacesFromSame
gatewayGroup = gatewayapi.Group(gatewayapi.GroupName)
gatewayKind = gatewayapi.Kind("Gateway")
simpleHostname = gatewayapi.Hostname("simple.local")
anyTestHostname = gatewayapi.Hostname("*.wildcard.local")

listenerReady = []kube_meta.Condition{
{
Expand All @@ -159,8 +256,9 @@ var (
},
}

simpleListenerName = gatewayapi.SectionName("simple")
simpleListener = gatewayapi.Listener{
simpleListenerName = gatewayapi.SectionName("simple")
anyHostnameListenerName = simpleListenerName
simpleListener = gatewayapi.Listener{
Name: simpleListenerName,
Port: gatewayapi.PortNumber(80),
Protocol: gatewayapi.HTTPProtocolType,
Expand All @@ -170,11 +268,25 @@ var (
},
},
}
allNsListenerName = gatewayapi.SectionName("allNS")
allNsListener = gatewayapi.Listener{
wildcardListenerName = gatewayapi.SectionName("wildcard")
wildcardListener = gatewayapi.Listener{
Name: wildcardListenerName,
Port: gatewayapi.PortNumber(80),
Protocol: gatewayapi.HTTPProtocolType,
Hostname: &anyTestHostname,
AllowedRoutes: &gatewayapi.AllowedRoutes{
Namespaces: &gatewayapi.RouteNamespaces{
From: &fromSame,
},
},
}
allNsListenerName = gatewayapi.SectionName("allNS")
simpleHostnameListenerName = allNsListenerName
allNsListener = gatewayapi.Listener{
Name: allNsListenerName,
Port: gatewayapi.PortNumber(80),
Protocol: gatewayapi.HTTPProtocolType,
Hostname: &simpleHostname,
AllowedRoutes: &gatewayapi.AllowedRoutes{
Namespaces: &gatewayapi.RouteNamespaces{
From: &fromAll,
Expand Down Expand Up @@ -223,6 +335,7 @@ var (
GatewayClassName: "kuma",
Listeners: []gatewayapi.Listener{
simpleListener,
wildcardListener,
allNsListener,
},
},
Expand All @@ -232,6 +345,10 @@ var (
Name: simpleListenerName,
Conditions: listenerReady,
},
{
Name: wildcardListenerName,
Conditions: listenerReady,
},
{
Name: allNsListenerName,
Conditions: listenerReady,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (r *HTTPRouteReconciler) gapiToKumaRoutes(

// Convert GAPI parent refs into selectors
for i, ref := range route.Spec.ParentRefs {
refAttachment, err := attachment.EvaluateParentRefAttachment(ctx, r.Client, &routeNs, ref)
refAttachment, err := attachment.EvaluateParentRefAttachment(ctx, r.Client, route.Spec.Hostnames, &routeNs, ref)
if err != nil {
return nil, nil, errors.Wrapf(err, "unable to check parent ref %d", i)
}
Expand Down