From 55ba4356f29b92d330bbfce86845c0749e3581e6 Mon Sep 17 00:00:00 2001 From: burner-account <15243175+burner-account@users.noreply.github.com> Date: Thu, 23 Jun 2022 11:58:09 +0200 Subject: [PATCH] Allow multiple listeners on same port in Gateway API provider --- ...ers_using_same_hostname_port_protocol.yml} | 38 ++++++++++++-- pkg/provider/kubernetes/gateway/kubernetes.go | 25 ++++++--- .../kubernetes/gateway/kubernetes_test.go | 51 ++++++++++++++++--- 3 files changed, 99 insertions(+), 15 deletions(-) rename pkg/provider/kubernetes/gateway/fixtures/mixed/{with_multiple_protocol_using_same_port.yml => with_multiple_listeners_using_same_hostname_port_protocol.yml} (76%) diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_protocol_using_same_port.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml similarity index 76% rename from pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_protocol_using_same_port.yml rename to pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml index 448dec2c7..dd3d32238 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_protocol_using_same_port.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml @@ -21,12 +21,13 @@ spec: kind: Gateway apiVersion: gateway.networking.k8s.io/v1alpha2 metadata: - name: my-gateway + name: my-gateway-http namespace: default spec: gatewayClassName: my-gateway-class listeners: # Use GatewayClass defaults for listener definition. - name: http1 + hostname: foo.bar protocol: HTTP port: 9080 allowedRoutes: @@ -37,6 +38,7 @@ spec: from: Same - name: http2 + hostname: foo.bar protocol: HTTP port: 9080 allowedRoutes: @@ -45,7 +47,26 @@ spec: namespaces: from: Same - - name: tcp +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: my-gateway-tcp + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tcp1 + hostname: foo.bar + protocol: TCP + port: 9000 + allowedRoutes: + kinds: + - kind: TCPRoute + namespaces: + from: Same + - name: tcp2 + hostname: foo.bar protocol: TCP port: 9000 allowedRoutes: @@ -54,7 +75,17 @@ spec: namespaces: from: Same - - name: tls +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: my-gateway-tls + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tls1 + hostname: foo.bar protocol: TLS port: 9000 tls: @@ -66,6 +97,7 @@ spec: from: Same - name: tls2 + hostname: foo.bar protocol: TLS port: 9000 tls: diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 05698f5f9..7b2bc5b72 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -317,7 +317,7 @@ func (p *Provider) createGatewayConf(ctx context.Context, client Client, gateway func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway *v1alpha2.Gateway, conf *dynamic.Configuration, tlsConfigs map[string]*tls.CertAndStores) []v1alpha2.ListenerStatus { logger := log.FromContext(ctx) listenerStatuses := make([]v1alpha2.ListenerStatus, len(gateway.Spec.Listeners)) - allocatedPort := map[v1alpha2.PortNumber]v1alpha2.ProtocolType{} + allocatedListeners := make(map[string]struct{}) for i, listener := range gateway.Spec.Listeners { listenerStatuses[i] = v1alpha2.ListenerStatus{ @@ -340,19 +340,22 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * continue } - if _, ok := allocatedPort[listener.Port]; ok { + listenerKey := makeListenerKey(listener) + + if _, ok := allocatedListeners[listenerKey]; ok { listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ - Type: string(v1alpha2.ListenerConditionDetached), + Type: string(v1alpha2.ListenerConditionConflicted), Status: metav1.ConditionTrue, LastTransitionTime: metav1.Now(), - Reason: string(v1alpha2.ListenerReasonPortUnavailable), - Message: fmt.Sprintf("Port %d unavailable", listener.Port), + Reason: "DuplicateListener", + Message: "A listener with same protocol, port and hostname already exists", }) continue } - allocatedPort[listener.Port] = listener.Protocol + allocatedListeners[listenerKey] = struct{}{} + ep, err := p.entryPointName(listener.Port, listener.Protocol) if err != nil { // update "Detached" status with "PortUnavailable" reason @@ -1700,3 +1703,13 @@ func isInternalService(ref v1alpha2.BackendRef) bool { *ref.Group == traefikv1alpha1.GroupName && strings.HasSuffix(string(ref.Name), "@internal") } + +// makeListenerKey joins protocol, hostname, and port of a listener into a string key. +func makeListenerKey(l v1alpha2.Listener) string { + var hostname v1alpha2.Hostname + if l.Hostname != nil { + hostname = *l.Hostname + } + + return fmt.Sprintf("%s|%s|%d", l.Protocol, hostname, l.Port) +} diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 2fafd65bb..a3ce54924 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -3566,8 +3566,8 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, { - desc: "Empty caused by mixed routes multiple protocol using same port", - paths: []string{"services.yml", "mixed/with_multiple_protocol_using_same_port.yml"}, + desc: "Empty caused by mixed routes with multiple listeners using same hostname, port and protocol", + paths: []string{"services.yml", "mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml"}, entryPoints: map[string]Entrypoint{ "web": {Address: ":9080"}, "tcp": {Address: ":9000"}, @@ -4989,7 +4989,7 @@ func Test_shouldAttach(t *testing.T) { } func Test_matchingHostnames(t *testing.T) { - tests := []struct { + testCases := []struct { desc string listener v1alpha2.Listener hostnames []v1alpha2.Hostname @@ -5081,7 +5081,7 @@ func Test_matchingHostnames(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -5093,7 +5093,7 @@ func Test_matchingHostnames(t *testing.T) { } func Test_getAllowedRoutes(t *testing.T) { - tests := []struct { + testCases := []struct { desc string listener v1alpha2.Listener supportedRouteKinds []v1alpha2.RouteGroupKind @@ -5193,7 +5193,7 @@ func Test_getAllowedRoutes(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -5210,6 +5210,45 @@ func Test_getAllowedRoutes(t *testing.T) { } } +func Test_makeListenerKey(t *testing.T) { + testCases := []struct { + desc string + listener v1alpha2.Listener + expectedKey string + }{ + { + desc: "empty", + expectedKey: "||0", + }, + { + desc: "listener with port, protocol and hostname", + listener: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + Hostname: hostnamePtr("www.example.com"), + }, + expectedKey: "HTTPS|www.example.com|443", + }, + { + desc: "listener with port, protocol and nil hostname", + listener: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + }, + expectedKey: "HTTPS||443", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expectedKey, makeListenerKey(test.listener)) + }) + } +} + func hostnamePtr(hostname v1alpha2.Hostname) *v1alpha2.Hostname { return &hostname }