diff --git a/apis/projectcontour/v1/detailedconditions.go b/apis/projectcontour/v1/detailedconditions.go index 2bfb7251405..d7ac6e13856 100644 --- a/apis/projectcontour/v1/detailedconditions.go +++ b/apis/projectcontour/v1/detailedconditions.go @@ -193,4 +193,8 @@ const ( // ConditionTypeVirtualHostError describes an error condition relating // to the VirtualHost configuration section of an HTTPProxy resource. ConditionTypeVirtualHostError = "VirtualHostError" + + // ConditionTypeListenerError describes an error condition relating + // to the configuration of Listeners. + ConditionTypeListenerError = "ListenerError" ) diff --git a/changelogs/unreleased/5160-skriss-major.md b/changelogs/unreleased/5160-skriss-major.md new file mode 100644 index 00000000000..0490c9a3204 --- /dev/null +++ b/changelogs/unreleased/5160-skriss-major.md @@ -0,0 +1,76 @@ +## Support for Gateway Listeners on more than two ports + +Contour now supports Gateway Listeners with many different ports. +Previously, Contour only allowed a single port for HTTP, and a single port for HTTPS/TLS. + +As an example, the following Gateway, with two HTTP ports and two HTTPS ports, is now fully supported: + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: contour +spec: + gatewayClassName: contour + listeners: + - name: http-1 + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + - name: http-2 + protocol: HTTP + port: 81 + allowedRoutes: + namespaces: + from: Same + - name: https-1 + protocol: HTTPS + port: 443 + allowedRoutes: + namespaces: + from: Same + tls: + mode: Terminate + certificateRefs: + - name: tls-cert-1 + - name: https-2 + protocol: HTTPS + port: 444 + allowedRoutes: + namespaces: + from: Same + tls: + mode: Terminate + certificateRefs: + - name: tls-cert-2 +``` + +If you are using the Contour Gateway Provisioner, ports for all valid Listeners will automatically be exposed via the Envoy service, and will update when any Listener changes are made. +If you are using static provisioning, you must keep the Service definition in sync with the Gateway spec manually. + +Note that if you are using the Contour Gateway Provisioner along with HTTPProxy or Ingress for routing, then your Gateway must have exactly one HTTP Listener and one HTTPS Listener. +For this case, Contour supports a custom HTTPS Listener protocol value, to avoid having to specify TLS details in the Listener (since they're specified in the HTTPProxy or Ingress instead): + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: contour-with-httpproxy +spec: + gatewayClassName: contour + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - name: https + protocol: projectcontour.io/https + port: 443 + allowedRoutes: + namespaces: + from: All +``` \ No newline at end of file diff --git a/cmd/contour/shutdownmanager.go b/cmd/contour/shutdownmanager.go index 1c8a48da5b3..f9dfd7e7874 100644 --- a/cmd/contour/shutdownmanager.go +++ b/cmd/contour/shutdownmanager.go @@ -24,7 +24,6 @@ import ( "time" "github.com/alecthomas/kingpin/v2" - xdscache_v3 "github.com/projectcontour/contour/internal/xdscache/v3" "github.com/prometheus/common/expfmt" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/wait" @@ -43,10 +42,6 @@ const shutdownReadyFile = "/admin/ok" // shutdownReadyCheckInterval is the default polling interval for the file used in the /shutdown endpoint. const shutdownReadyCheckInterval = time.Second * 1 -func prometheusLabels() []string { - return []string{xdscache_v3.ENVOY_HTTP_LISTENER, xdscache_v3.ENVOY_HTTPS_LISTENER} -} - type shutdownmanagerContext struct { // httpServePort defines what port the shutdown-manager listens on httpServePort int @@ -273,10 +268,11 @@ func parseOpenConnections(stats io.Reader) (int, error) { // Look up open connections value for _, metrics := range metricFamilies[prometheusStat].Metric { for _, labels := range metrics.Label { - for _, item := range prometheusLabels() { - if item == labels.GetValue() { - openConnections += int(metrics.Gauge.GetValue()) - } + switch labels.GetValue() { + // don't count connections to these listeners. + case "admin", "envoy-admin", "stats", "health", "stats-health": + default: + openConnections += int(metrics.Gauge.GetValue()) } } } diff --git a/cmd/contour/shutdownmanager_test.go b/cmd/contour/shutdownmanager_test.go index 327035c32d7..a809676afe6 100644 --- a/cmd/contour/shutdownmanager_test.go +++ b/cmd/contour/shutdownmanager_test.go @@ -177,6 +177,12 @@ func TestParseOpenConnections(t *testing.T) { wantError: nil, }) + run(t, "many listeners", testcase{ + stats: strings.NewReader(VALIDMANYLISTENERS), + wantConnections: 16, + wantError: nil, + }) + run(t, "missing values", testcase{ stats: strings.NewReader(MISSING_STATS), wantConnections: -1, @@ -218,6 +224,10 @@ envoy_server_days_until_first_cert_expiring{} 82 envoy_server_hot_restart_epoch{} 0 # TYPE envoy_http_downstream_cx_active gauge envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="ingress_http"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="envoy-admin"} 7 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats"} 77 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="health"} 777 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats-health"} 7777 ` VALIDHTTPS = `envoy_cluster_circuit_breakers_default_cx_pool_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 envoy_cluster_max_host_weight{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 @@ -244,7 +254,11 @@ envoy_server_days_until_first_cert_expiring{} 82 # TYPE envoy_server_hot_restart_epoch gauge envoy_server_hot_restart_epoch{} 0 # TYPE envoy_http_downstream_cx_active gauge -envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="ingress_http"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="ingress_https"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="envoy-admin"} 7 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats"} 77 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="health"} 777 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats-health"} 7777 ` VALIDBOTH = `envoy_cluster_circuit_breakers_default_cx_pool_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 envoy_cluster_max_host_weight{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 @@ -273,6 +287,45 @@ envoy_server_hot_restart_epoch{} 0 # TYPE envoy_http_downstream_cx_active gauge envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="ingress_http"} 4 envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="ingress_https"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="envoy-admin"} 7 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats"} 77 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="health"} 777 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats-health"} 7777 +` + + VALIDMANYLISTENERS = `envoy_cluster_circuit_breakers_default_cx_pool_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_max_host_weight{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_upstream_rq_pending_active{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_circuit_breakers_high_rq_retry_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_circuit_breakers_high_cx_pool_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_upstream_cx_tx_bytes_buffered{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_version{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +envoy_cluster_circuit_breakers_default_cx_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 +# TYPE envoy_http_downstream_cx_ssl_active gauge +envoy_http_downstream_cx_ssl_active{envoy_http_conn_manager_prefix="admin"} 0 +# TYPE envoy_server_total_connections gauge +envoy_server_total_connections{} 1 +# TYPE envoy_runtime_num_layers gauge +envoy_runtime_num_layers{} 2 +# TYPE envoy_server_parent_connections gauge +envoy_server_parent_connections{} 0 +# TYPE envoy_server_stats_recent_lookups gauge +envoy_server_stats_recent_lookups{} 0 +# TYPE envoy_cluster_manager_warming_clusters gauge +envoy_cluster_manager_warming_clusters{} 0 +# TYPE envoy_server_days_until_first_cert_expiring gauge +envoy_server_days_until_first_cert_expiring{} 82 +# TYPE envoy_server_hot_restart_epoch gauge +envoy_server_hot_restart_epoch{} 0 +# TYPE envoy_http_downstream_cx_active gauge +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="http-80"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="http-81"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="https-443"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="https-444"} 4 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="envoy-admin"} 7 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats"} 77 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="health"} 777 +envoy_http_downstream_cx_active{envoy_http_conn_manager_prefix="stats-health"} 7777 ` MISSING_STATS = `envoy_cluster_circuit_breakers_default_cx_pool_open{envoy_cluster_name="projectcontour_envoy-admin_9001"} 0 diff --git a/internal/dag/accessors.go b/internal/dag/accessors.go index f2ba82ddb25..0a1209858c4 100644 --- a/internal/dag/accessors.go +++ b/internal/dag/accessors.go @@ -16,6 +16,7 @@ package dag import ( "fmt" "strconv" + "strings" "github.com/projectcontour/contour/internal/annotation" "github.com/projectcontour/contour/internal/xds" @@ -118,6 +119,30 @@ func externalName(svc *v1.Service) string { return svc.Spec.ExternalName } +// GetSingleListener returns the sole listener with the specified protocol, +// or an error if there is not exactly one listener with that protocol. +func (d *DAG) GetSingleListener(protocol string) (*Listener, error) { + var res *Listener + + for _, listener := range d.Listeners { + if listener.Protocol != protocol { + continue + } + + if res != nil { + return nil, fmt.Errorf("more than one %s listener configured", strings.ToUpper(protocol)) + } + + res = listener + } + + if res == nil { + return nil, fmt.Errorf("no %s listener configured", strings.ToUpper(protocol)) + } + + return res, nil +} + // GetSecureVirtualHost returns the secure virtual host in the DAG that // matches the provided name, or nil if no matching secure virtual host // is found. diff --git a/internal/dag/accessors_test.go b/internal/dag/accessors_test.go index 66d5b0dd7aa..c9ab5640342 100644 --- a/internal/dag/accessors_test.go +++ b/internal/dag/accessors_test.go @@ -196,3 +196,78 @@ func TestBuilderLookupService(t *testing.T) { }) } } + +func TestGetSingleListener(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + d := &DAG{ + Listeners: map[string]*Listener{ + "http": { + Protocol: "http", + Port: 80, + }, + "https": { + Protocol: "https", + Port: 443, + }, + }, + } + + got, gotErr := d.GetSingleListener("http") + assert.Equal(t, d.Listeners["http"], got) + assert.NoError(t, gotErr) + + got, gotErr = d.GetSingleListener("https") + assert.Equal(t, d.Listeners["https"], got) + assert.NoError(t, gotErr) + }) + + t.Run("one HTTP listener, no HTTPS listener", func(t *testing.T) { + d := &DAG{ + Listeners: map[string]*Listener{ + "http": { + Protocol: "http", + Port: 80, + }, + }, + } + + got, gotErr := d.GetSingleListener("http") + assert.Equal(t, d.Listeners["http"], got) + assert.NoError(t, gotErr) + + got, gotErr = d.GetSingleListener("https") + assert.Nil(t, got) + assert.EqualError(t, gotErr, "no HTTPS listener configured") + }) + + t.Run("many HTTP listeners, one HTTPS listener", func(t *testing.T) { + d := &DAG{ + Listeners: map[string]*Listener{ + "http-1": { + Protocol: "http", + Port: 80, + }, + "http-2": { + Protocol: "http", + Port: 81, + }, + "http-3": { + Protocol: "http", + Port: 82, + }, + "https-1": { + Protocol: "https", + Port: 443, + }, + }, + } + + got, gotErr := d.GetSingleListener("http") + assert.Nil(t, got) + assert.EqualError(t, gotErr, "more than one HTTP listener configured") + + got, gotErr = d.GetSingleListener("https") + assert.Equal(t, d.Listeners["https-1"], got) + assert.NoError(t, gotErr) + }) +} diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index 8751b8df873..bca2230897a 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -275,7 +275,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { Spec: gatewayapi_v1beta1.GatewaySpec{ GatewayClassName: gatewayapi_v1beta1.ObjectName(validClass.Name), Listeners: []gatewayapi_v1beta1.Listener{{ - Port: 80, + Port: 443, Protocol: gatewayapi_v1beta1.TLSProtocolType, TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), @@ -297,7 +297,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { Spec: gatewayapi_v1beta1.GatewaySpec{ GatewayClassName: gatewayapi_v1beta1.ObjectName(validClass.Name), Listeners: []gatewayapi_v1beta1.Listener{{ - Port: 80, + Port: 443, Protocol: gatewayapi_v1beta1.TLSProtocolType, TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), @@ -319,7 +319,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { Spec: gatewayapi_v1beta1.GatewaySpec{ GatewayClassName: gatewayapi_v1beta1.ObjectName(validClass.Name), Listeners: []gatewayapi_v1beta1.Listener{{ - Port: 80, + Port: 443, Protocol: gatewayapi_v1beta1.TLSProtocolType, TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), @@ -510,8 +510,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), ), @@ -544,8 +543,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), ), @@ -613,8 +611,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardServiceCustomNs))), ), @@ -711,8 +708,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -749,8 +745,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -816,8 +811,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -1050,8 +1044,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("another.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), @@ -1159,8 +1152,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost( "test.projectcontour.io", @@ -1217,8 +1209,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), virtualhost("test2.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), @@ -1251,8 +1242,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", prefixrouteHTTPRoute("/", service(kuardService))), ), @@ -1285,8 +1275,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("*.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -1398,8 +1387,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1437,8 +1425,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1477,8 +1464,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1532,8 +1518,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("*", prefixrouteHTTPRoute("/", service(kuardService)))), }), }, @@ -1586,8 +1571,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("*", prefixrouteHTTPRoute("/", service(kuardService)))), }), }, @@ -1639,8 +1623,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1694,8 +1677,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1749,8 +1731,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1805,8 +1786,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRoute("/", http.StatusInternalServerError)), ), @@ -1838,8 +1818,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", exactrouteHTTPRoute("/blog", service(kuardService))), @@ -1873,8 +1852,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", regexrouteHTTPRoute("/bl+og", service(kuardService))), @@ -1924,8 +1902,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService)), @@ -1966,8 +1943,10 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-443", + Protocol: "http", + Address: "0.0.0.0", + Port: 8443, VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService)), @@ -2011,8 +1990,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -2061,8 +2039,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -2074,8 +2051,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { ), }, &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), ), @@ -2172,8 +2148,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -2223,8 +2198,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -2513,8 +2487,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -2526,8 +2499,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { ), }, &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", prefixrouteHTTPRoute("/", service(kuardService))), ), @@ -2566,8 +2538,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2620,8 +2591,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/blog"), @@ -2666,8 +2636,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2713,8 +2682,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2760,8 +2728,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2811,8 +2778,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2863,8 +2829,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2930,8 +2895,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -2999,7 +2963,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, + Name: "http-80", Port: 8080, VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ @@ -3059,7 +3023,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, + Name: "http-80", Port: 8080, VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ @@ -3112,7 +3076,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, + Name: "http-80", Port: 8080, VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ @@ -3181,8 +3145,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3257,8 +3220,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3340,8 +3302,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3413,8 +3374,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3463,8 +3423,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3525,8 +3484,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3582,8 +3540,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3627,8 +3584,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3680,8 +3636,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixString("/"), @@ -3740,8 +3695,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -3790,8 +3744,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -3840,8 +3793,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -3888,8 +3840,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", withMirror(prefixrouteHTTPRoute("/", service(kuardService)), service(kuardService2)))), }, @@ -3931,8 +3882,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", withMirror(prefixrouteHTTPRoute("/", service(kuardService)), service(kuardService2)), withMirror(segmentPrefixHTTPRoute("/another-match", service(kuardService)), service(kuardService2)), @@ -3975,8 +3925,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -4024,8 +3973,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -4073,8 +4021,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -4119,8 +4066,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -4176,8 +4122,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: prefixSegment("/prefix"), @@ -4222,8 +4167,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", prefixrouteHTTPRoute("/", &Service{ @@ -4287,8 +4231,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", prefixrouteHTTPRoute("/", &Service{ @@ -4348,8 +4291,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("*", directResponseRouteService("/", http.StatusInternalServerError, &Service{ Weighted: WeightedService{ @@ -4387,8 +4329,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -4474,8 +4415,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -4539,8 +4479,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -4778,8 +4717,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -4836,8 +4774,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -4908,8 +4845,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -4974,8 +4910,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -5023,8 +4958,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -5072,8 +5006,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -5115,8 +5048,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("gateway.projectcontour.io", exactrouteHTTPRoute("/blog", service(kuardService))), @@ -5150,8 +5082,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("http.projectcontour.io", exactrouteHTTPRoute("/blog", service(kuardService))), @@ -5168,8 +5099,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", exactrouteGRPCRoute("/io.projectcontour/Login", grpcService(kuardService, "h2c"))), ), @@ -5288,8 +5218,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTPS_LISTENER_NAME, - Port: 8443, + Name: "https-443", SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ @@ -5301,8 +5230,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { ), }, &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts( virtualhost("test.projectcontour.io", exactrouteGRPCRoute("/io.projectcontour/Login", grpcService(kuardService, "h2c"))), ), @@ -5338,8 +5266,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: exact("/io.projectcontour/Login"), @@ -5381,8 +5308,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: exact("/io.projectcontour/Login"), @@ -5423,8 +5349,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: &PrefixMatchCondition{Prefix: "/"}, @@ -5463,8 +5388,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: &PrefixMatchCondition{Prefix: "/"}, @@ -5532,8 +5456,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: exact("/io.projectcontour/Login"), @@ -5610,8 +5533,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: exact("/io.projectcontour/Login"), @@ -5672,8 +5594,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: exact("/io.projectcontour/Login"), @@ -5736,8 +5657,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", &Route{ PathMatchCondition: exact("/io.projectcontour/Login"), @@ -5782,8 +5702,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners( &Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io", withMirror(exactrouteGRPCRoute("/io.projectcontour/Login", grpcService(kuardService, "h2c")), grpcService(kuardService2, "h2c")))), }, @@ -5840,8 +5759,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, }, want: listeners(&Listener{ - Name: HTTP_LISTENER_NAME, - Port: 8080, + Name: "http-80", VirtualHosts: virtualhosts(virtualhost("*", exactrouteGRPCRoute("/io.projectcontour/Login", grpcService(kuardService, "h2c")))), }), }, @@ -14217,6 +14135,903 @@ func TestDAGInsert(t *testing.T) { } } +func TestGatewayWithHTTPProxyAndIngress(t *testing.T) { + kuardService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kuard", + Namespace: "projectcontour", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: "TCP", + Port: 8080, + TargetPort: intstr.FromInt(8080), + }}, + }, + } + sec1 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "projectcontour", + }, + Type: v1.SecretTypeTLS, + Data: secretdata(fixture.CERTIFICATE, fixture.RSA_PRIVATE_KEY), + } + + tests := map[string]struct { + gateway *gatewayapi_v1beta1.Gateway + objs []any + want []*Listener + }{ + "HTTPProxy attached to HTTP-only Gateway": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", prefixroute("/", service(kuardService))), + ), + }, + ), + }, + "HTTPProxy attached to Gateway with multiple HTTP listeners": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 81, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: nil, + }, + "HTTPProxy attached to Gateway with HTTP and HTTPS listener": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "https-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", prefixroute("/", service(kuardService))), + ), + }, + ), + }, + "HTTPProxy with TLS attached to Gateway with HTTP and HTTPS listener": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "https-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + sec1, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + TLS: &contour_api_v1.TLS{ + SecretName: sec1.Name, + }, + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", routeUpgrade("/", service(kuardService))), + ), + }, + &Listener{ + Name: "https-443", + SecureVirtualHosts: []*SecureVirtualHost{ + securevirtualhost("kuard.projectcontour.io", sec1, routeUpgrade("/", service(kuardService))), + }, + }, + ), + }, + "HTTPProxy with TLS attached to Gateway with HTTP and HTTPS listener using projectcontour.io/https protocol": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "https-1", + Protocol: gatewayapi.ContourHTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + sec1, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + TLS: &contour_api_v1.TLS{ + SecretName: sec1.Name, + }, + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", routeUpgrade("/", service(kuardService))), + ), + }, + &Listener{ + Name: "https-443", + SecureVirtualHosts: []*SecureVirtualHost{ + securevirtualhost("kuard.projectcontour.io", sec1, routeUpgrade("/", service(kuardService))), + }, + }, + ), + }, + "HTTPProxy with TLS attached to Gateway with no HTTPS listener": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + sec1, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + TLS: &contour_api_v1.TLS{ + SecretName: sec1.Name, + }, + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: nil, + }, + + "Ingress attached to HTTP-only Gateway": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + &networking_v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-ingress", + }, + Spec: networking_v1.IngressSpec{ + Rules: []networking_v1.IngressRule{ + { + Host: "kuard.projectcontour.io", + IngressRuleValue: networking_v1.IngressRuleValue{ + HTTP: &networking_v1.HTTPIngressRuleValue{ + Paths: []networking_v1.HTTPIngressPath{ + { + Backend: *backendv1("kuard", intstr.FromInt(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", prefixroute("/", service(kuardService))), + ), + }, + ), + }, + "Ingress attached to Gateway with multiple HTTP listeners": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 81, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + &networking_v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-ingress", + }, + Spec: networking_v1.IngressSpec{ + Rules: []networking_v1.IngressRule{ + { + Host: "kuard.projectcontour.io", + IngressRuleValue: networking_v1.IngressRuleValue{ + HTTP: &networking_v1.HTTPIngressRuleValue{ + Paths: []networking_v1.HTTPIngressPath{ + { + Backend: *backendv1("kuard", intstr.FromInt(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: nil, + }, + "Ingress attached to Gateway with HTTP and HTTPS listener": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "https-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + &networking_v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-ingress", + }, + Spec: networking_v1.IngressSpec{ + Rules: []networking_v1.IngressRule{ + { + Host: "kuard.projectcontour.io", + IngressRuleValue: networking_v1.IngressRuleValue{ + HTTP: &networking_v1.HTTPIngressRuleValue{ + Paths: []networking_v1.HTTPIngressPath{ + { + Backend: *backendv1("kuard", intstr.FromInt(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", prefixroute("/", service(kuardService))), + ), + }, + ), + }, + "Ingress with TLS attached to Gateway with HTTP and HTTPS listener": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "https-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + sec1, + &networking_v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-ingress", + }, + Spec: networking_v1.IngressSpec{ + Rules: []networking_v1.IngressRule{ + { + Host: "kuard.projectcontour.io", + IngressRuleValue: networking_v1.IngressRuleValue{ + HTTP: &networking_v1.HTTPIngressRuleValue{ + Paths: []networking_v1.HTTPIngressPath{ + { + Backend: *backendv1("kuard", intstr.FromInt(8080)), + }, + }, + }, + }, + }, + }, + TLS: []networking_v1.IngressTLS{ + { + SecretName: sec1.Name, + Hosts: []string{"kuard.projectcontour.io"}, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", prefixroute("/", service(kuardService))), + ), + }, + &Listener{ + Name: "https-443", + SecureVirtualHosts: []*SecureVirtualHost{ + securevirtualhost("kuard.projectcontour.io", sec1, prefixroute("/", service(kuardService))), + }, + }, + ), + }, + "Ingress with TLS attached to Gateway with HTTP and HTTPS listener using projectcontour.io/https protocol": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "https-1", + Protocol: gatewayapi.ContourHTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + sec1, + &networking_v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-ingress", + }, + Spec: networking_v1.IngressSpec{ + Rules: []networking_v1.IngressRule{ + { + Host: "kuard.projectcontour.io", + IngressRuleValue: networking_v1.IngressRuleValue{ + HTTP: &networking_v1.HTTPIngressRuleValue{ + Paths: []networking_v1.HTTPIngressPath{ + { + Backend: *backendv1("kuard", intstr.FromInt(8080)), + }, + }, + }, + }, + }, + }, + TLS: []networking_v1.IngressTLS{ + { + SecretName: sec1.Name, + Hosts: []string{"kuard.projectcontour.io"}, + }, + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("kuard.projectcontour.io", prefixroute("/", service(kuardService))), + ), + }, + &Listener{ + Name: "https-443", + SecureVirtualHosts: []*SecureVirtualHost{ + securevirtualhost("kuard.projectcontour.io", sec1, prefixroute("/", service(kuardService))), + }, + }, + ), + }, + "Ingress with TLS (with HTTP not allowed) attached to Gateway with no HTTPS listener": { + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + objs: []any{ + kuardService, + sec1, + &networking_v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "kuard-ingress", + Annotations: map[string]string{ + "kubernetes.io/ingress.allow-http": "false", + }, + }, + Spec: networking_v1.IngressSpec{ + Rules: []networking_v1.IngressRule{ + { + Host: "kuard.projectcontour.io", + IngressRuleValue: networking_v1.IngressRuleValue{ + HTTP: &networking_v1.HTTPIngressRuleValue{ + Paths: []networking_v1.HTTPIngressPath{ + { + Backend: *backendv1("kuard", intstr.FromInt(8080)), + }, + }, + }, + }, + }, + }, + TLS: []networking_v1.IngressTLS{ + { + SecretName: sec1.Name, + Hosts: []string{"kuard.projectcontour.io"}, + }, + }, + }, + }, + }, + want: nil, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + gc := &gatewayapi_v1beta1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "contour-gc", + }, + Spec: gatewayapi_v1beta1.GatewayClassSpec{ + ControllerName: "projectcontour.io/gateway-controller", + }, + } + + builder := Builder{ + Source: KubernetesCache{ + gatewayclass: gc, + gateway: tc.gateway, + FieldLogger: fixture.NewTestLogger(t), + }, + Processors: []Processor{ + &ListenerProcessor{}, + &IngressProcessor{ + FieldLogger: fixture.NewTestLogger(t), + }, + &HTTPProxyProcessor{}, + }, + } + + for _, o := range tc.objs { + builder.Source.Insert(o) + } + dag := builder.Build() + + got := make(map[int]*Listener) + for _, l := range dag.Listeners { + got[l.Port] = l + } + + want := make(map[int]*Listener) + for _, v := range tc.want { + want[v.Port] = v + } + assert.Equal(t, want, got) + }) + } +} + func backendv1(name string, port intstr.IntOrString) *networking_v1.IngressBackend { var v1port networking_v1.ServiceBackendPort @@ -15373,9 +16188,19 @@ func listeners(ls ...*Listener) []*Listener { switch listener.Name { case HTTP_LISTENER_NAME: listener.RouteConfigName = "ingress_http" + listener.Protocol = "http" case HTTPS_LISTENER_NAME: listener.RouteConfigName = "https" listener.FallbackCertRouteConfigName = "ingress_fallbackcert" + listener.Protocol = "https" + case "http-80": + listener.Protocol = "http" + listener.Address = "0.0.0.0" + listener.Port = 8080 + case "https-443": + listener.Protocol = "https" + listener.Address = "0.0.0.0" + listener.Port = 8443 } } diff --git a/internal/dag/dag.go b/internal/dag/dag.go index ac53727f587..ebc5dd0b6ff 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -855,6 +855,10 @@ type Listener struct { // Name is the unique name of the listener. Name string + // Protocol is the listener protocol. Must be + // "http" or "https". + Protocol string + // Address is the TCP address to listen on. // If blank 0.0.0.0, or ::/0 for IPv6, is assumed. Address string diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index 894b1dfed15..6b52d6e5824 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -211,7 +211,7 @@ func (p *GatewayAPIProcessor) processRoute( routeHostnames = route.Spec.Hostnames } - hosts, errs := p.computeHosts(routeHostnames, gatewayapi.HostnameDeref(listener.listener.Hostname)) + hosts, errs := p.computeHosts(routeHostnames, string(ref.Val(listener.listener.Hostname, ""))) for _, err := range errs { // The Gateway API spec does not indicate what to do if syntactically // invalid hostnames make it through, we're using our best judgment here. @@ -341,6 +341,7 @@ func (p *GatewayAPIProcessor) getListenersForRouteParentRef( type listenerInfo struct { listener gatewayapi_v1beta1.Listener + dagListenerName string allowedKinds []gatewayapi_v1beta1.Kind namespaceSelector labels.Selector tlsSecret *Secret @@ -543,6 +544,7 @@ func (p *GatewayAPIProcessor) computeListener( return true, &listenerInfo{ listener: listener, + dagListenerName: validateListenersResult.ListenerNames[string(listener.Name)], allowedKinds: listenerRouteKinds, tlsSecret: listenerSecret, namespaceSelector: selector, @@ -956,7 +958,7 @@ func (p *GatewayAPIProcessor) computeTLSRouteForListener(route *gatewayapi_v1alp } for host := range hosts { - secure := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + secure := p.dag.EnsureSecureVirtualHost(listener.dagListenerName, host) if listener.tlsSecret != nil { secure.Secret = listener.tlsSecret @@ -1286,11 +1288,11 @@ func (p *GatewayAPIProcessor) computeHTTPRouteForListener(route *gatewayapi_v1be for _, route := range routes { switch { case listener.tlsSecret != nil: - svhost := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + svhost := p.dag.EnsureSecureVirtualHost(listener.dagListenerName, host) svhost.Secret = listener.tlsSecret svhost.AddRoute(route) default: - vhost := p.dag.EnsureVirtualHost(HTTP_LISTENER_NAME, host) + vhost := p.dag.EnsureVirtualHost(listener.dagListenerName, host) vhost.AddRoute(route) } @@ -1416,11 +1418,11 @@ func (p *GatewayAPIProcessor) computeGRPCRouteForListener(route *gatewayapi_v1al for _, route := range routes { switch { case listener.tlsSecret != nil: - svhost := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + svhost := p.dag.EnsureSecureVirtualHost(listener.dagListenerName, host) svhost.Secret = listener.tlsSecret svhost.AddRoute(route) default: - vhost := p.dag.EnsureVirtualHost(HTTP_LISTENER_NAME, host) + vhost := p.dag.EnsureVirtualHost(listener.dagListenerName, host) vhost.AddRoute(route) } diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index b74138ff35c..d1b2b61742f 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -228,7 +228,13 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { return } - svhost := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + listener, err := p.dag.GetSingleListener("https") + if err != nil { + validCond.AddError(contour_api_v1.ConditionTypeListenerError, "ErrorIdentifyingListener", err.Error()) + return + } + + svhost := p.dag.EnsureSecureVirtualHost(listener.Name, host) svhost.Secret = sec // default to a minimum TLS version of 1.2 if it's not specified svhost.MinTLSVersion = annotation.MinTLSVersion(tls.MinimumProtocolVersion, "1.2") @@ -475,7 +481,15 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { } routes := p.computeRoutes(validCond, proxy, proxy, nil, nil, tlsEnabled, defaultJWTProvider) - insecure := p.dag.EnsureVirtualHost(HTTP_LISTENER_NAME, host) + + listener, err := p.dag.GetSingleListener("http") + if err != nil { + validCond.AddError(contour_api_v1.ConditionTypeListenerError, "ErrorIdentifyingListener", err.Error()) + return + } + + insecure := p.dag.EnsureVirtualHost(listener.Name, host) + cp, err := toCORSPolicy(proxy.Spec.VirtualHost.CORSPolicy) if err != nil { validCond.AddErrorf(contour_api_v1.ConditionTypeCORSError, "PolicyDidNotParse", @@ -508,7 +522,13 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { // if TLS is enabled for this virtual host and there is no tcp proxy defined, // then add routes to the secure virtualhost definition. if tlsEnabled && proxy.Spec.TCPProxy == nil { - secure := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + listener, err := p.dag.GetSingleListener("https") + if err != nil { + validCond.AddError(contour_api_v1.ConditionTypeListenerError, "ErrorIdentifyingListener", err.Error()) + return + } + + secure := p.dag.EnsureSecureVirtualHost(listener.Name, host) secure.CORSPolicy = cp rlp, err := rateLimitPolicy(proxy.Spec.VirtualHost.RateLimitPolicy) @@ -1137,7 +1157,14 @@ func (p *HTTPProxyProcessor) processHTTPProxyTCPProxy(validCond *contour_api_v1. TimeoutPolicy: ClusterTimeoutPolicy{ConnectTimeout: p.ConnectTimeout}, }) } - secure := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + + listener, err := p.dag.GetSingleListener("https") + if err != nil { + validCond.AddError(contour_api_v1.ConditionTypeListenerError, "ErrorIdentifyingListener", err.Error()) + return false + } + + secure := p.dag.EnsureSecureVirtualHost(listener.Name, host) secure.TCPProxy = &proxy return true diff --git a/internal/dag/ingress_processor.go b/internal/dag/ingress_processor.go index 2789f92ddbc..8c94909410c 100644 --- a/internal/dag/ingress_processor.go +++ b/internal/dag/ingress_processor.go @@ -103,7 +103,16 @@ func (p *IngressProcessor) computeSecureVirtualhosts() { // ahead and create the SecureVirtualHost for this // Ingress. for _, host := range tls.Hosts { - svhost := p.dag.EnsureSecureVirtualHost(HTTPS_LISTENER_NAME, host) + listener, err := p.dag.GetSingleListener("https") + if err != nil { + p.WithError(err). + WithField("name", ing.GetName()). + WithField("namespace", ing.GetNamespace()). + Errorf("error identifying listener") + return + } + + svhost := p.dag.EnsureSecureVirtualHost(listener.Name, host) svhost.Secret = sec // default to a minimum TLS version of 1.2 if it's not specified svhost.MinTLSVersion = annotation.MinTLSVersion(annotation.ContourAnnotation(ing, "tls-minimum-protocol-version"), "1.2") @@ -189,14 +198,32 @@ func (p *IngressProcessor) computeIngressRule(ing *networking_v1.Ingress, rule n // should we create port 80 routes for this ingress if annotation.TLSRequired(ing) || annotation.HTTPAllowed(ing) { - vhost := p.dag.EnsureVirtualHost(HTTP_LISTENER_NAME, host) + listener, err := p.dag.GetSingleListener("http") + if err != nil { + p.WithError(err). + WithField("name", ing.GetName()). + WithField("namespace", ing.GetNamespace()). + Errorf("error identifying listener") + return + } + + vhost := p.dag.EnsureVirtualHost(listener.Name, host) vhost.AddRoute(r) } + listener, err := p.dag.GetSingleListener("https") + if err != nil { + p.WithError(err). + WithField("name", ing.GetName()). + WithField("namespace", ing.GetNamespace()). + Errorf("error identifying listener") + return + } + // computeSecureVirtualhosts will have populated b.securevirtualhosts // with the names of tls enabled ingress objects. If host exists then // it is correctly configured for TLS. - if svh := p.dag.GetSecureVirtualHost(HTTPS_LISTENER_NAME, host); svh != nil && host != "*" { + if svh := p.dag.GetSecureVirtualHost(listener.Name, host); svh != nil && host != "*" { svh.AddRoute(r) } } diff --git a/internal/dag/listener_processor.go b/internal/dag/listener_processor.go index 30ab81ea3e2..477fb69ef14 100644 --- a/internal/dag/listener_processor.go +++ b/internal/dag/listener_processor.go @@ -13,6 +13,8 @@ package dag +import "github.com/projectcontour/contour/internal/gatewayapi" + // nolint:revive const ( HTTP_LISTENER_NAME = "ingress_http" @@ -30,21 +32,38 @@ type ListenerProcessor struct { // Run adds HTTP and HTTPS listeners to the DAG. func (p *ListenerProcessor) Run(dag *DAG, cache *KubernetesCache) { - dag.Listeners[HTTP_LISTENER_NAME] = &Listener{ - Name: HTTP_LISTENER_NAME, - Address: p.HTTPAddress, - Port: intOrDefault(p.HTTPPort, 8080), - RouteConfigName: "ingress_http", - vhostsByName: map[string]*VirtualHost{}, - } + if cache.gateway != nil { + dag.HasDynamicListeners = true + + for _, port := range gatewayapi.ValidateListeners(cache.gateway.Spec.Listeners).Ports { + dag.Listeners[port.Name] = &Listener{ + Name: port.Name, + Protocol: port.Protocol, + Address: "0.0.0.0", + Port: int(port.ContainerPort), + vhostsByName: map[string]*VirtualHost{}, + svhostsByName: map[string]*SecureVirtualHost{}, + } + } + } else { + dag.Listeners[HTTP_LISTENER_NAME] = &Listener{ + Name: HTTP_LISTENER_NAME, + Protocol: "http", + Address: p.HTTPAddress, + Port: intOrDefault(p.HTTPPort, 8080), + RouteConfigName: "ingress_http", + vhostsByName: map[string]*VirtualHost{}, + } - dag.Listeners[HTTPS_LISTENER_NAME] = &Listener{ - Name: HTTPS_LISTENER_NAME, - Address: p.HTTPSAddress, - Port: intOrDefault(p.HTTPSPort, 8443), - RouteConfigName: "https", - FallbackCertRouteConfigName: "ingress_fallbackcert", - svhostsByName: map[string]*SecureVirtualHost{}, + dag.Listeners[HTTPS_LISTENER_NAME] = &Listener{ + Name: HTTPS_LISTENER_NAME, + Protocol: "https", + Address: p.HTTPSAddress, + Port: intOrDefault(p.HTTPSPort, 8443), + RouteConfigName: "https", + FallbackCertRouteConfigName: "ingress_fallbackcert", + svhostsByName: map[string]*SecureVirtualHost{}, + } } } diff --git a/internal/dag/status_test.go b/internal/dag/status_test.go index d5ab99b85a1..09f125c2140 100644 --- a/internal/dag/status_test.go +++ b/internal/dag/status_test.go @@ -4659,6 +4659,167 @@ func TestDAGStatus(t *testing.T) { Valid(), }, }) + + run(t, "HTTPProxy cannot attach to a Gateway with >1 HTTP Listener", testcase{ + objs: []any{ + &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 81, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kuard", + Namespace: "roots", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: "TCP", + Port: 8080, + TargetPort: intstr.FromInt(8080), + }}, + }, + }, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "roots", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: map[types.NamespacedName]contour_api_v1.DetailedCondition{ + {Namespace: "roots", Name: "kuard-proxy"}: fixture.NewValidCondition(). + WithError( + contour_api_v1.ConditionTypeListenerError, + "ErrorIdentifyingListener", + "more than one HTTP listener configured", + ), + }, + }) + + run(t, "HTTPProxy cannot attach to a Gateway with no HTTP Listener", testcase{ + objs: []any{ + &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "projectcontour", + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: "contour-gc", + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "https-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModePassthrough), + }, + }, + }, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kuard", + Namespace: "roots", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: "TCP", + Port: 8080, + TargetPort: intstr.FromInt(8080), + }}, + }, + }, + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "roots", + Name: "kuard-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.projectcontour.io", + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{ + { + Prefix: "/", + }, + }, + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 8080, + }, + }, + }, + }, + }, + }, + }, + want: map[types.NamespacedName]contour_api_v1.DetailedCondition{ + {Namespace: "roots", Name: "kuard-proxy"}: fixture.NewValidCondition(). + WithError( + contour_api_v1.ConditionTypeListenerError, + "ErrorIdentifyingListener", + "no HTTP listener configured", + ), + }, + }) } func validGatewayStatusUpdate(listenerName string, kind gatewayapi_v1beta1.Kind, attachedRoutes int) []*status.GatewayStatusUpdate { @@ -8407,7 +8568,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), Status: metav1.ConditionFalse, Reason: string(gatewayapi_v1beta1.ListenerReasonUnsupportedProtocol), - Message: "Listener protocol \"invalid\" is unsupported, must be one of HTTP, HTTPS or TLS", + Message: "Listener protocol \"invalid\" is unsupported, must be one of HTTP, HTTPS, TLS or projectcontour.io/https", }, }, }, diff --git a/internal/featuretests/v3/envoy.go b/internal/featuretests/v3/envoy.go index b4f42a21763..8c4659c5b85 100644 --- a/internal/featuretests/v3/envoy.go +++ b/internal/featuretests/v3/envoy.go @@ -449,6 +449,16 @@ func httpsFilterFor(vhost string) *envoy_listener_v3.Filter { Get() } +func httpsFilterForGateway(listener, vhost string) *envoy_listener_v3.Filter { + return envoy_v3.HTTPConnectionManagerBuilder(). + AddFilter(envoy_v3.FilterMisdirectedRequests(vhost)). + DefaultFilters(). + RouteConfigName(path.Join(listener, vhost)). + MetricsPrefix(listener). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + Get() +} + // httpsFilterWithXfccFor does the same as httpsFilterFor but enable // client certs details forwarding func httpsFilterWithXfccFor(vhost string, d *dag.ClientCertificateDetails) *envoy_listener_v3.Filter { diff --git a/internal/featuretests/v3/httproute_test.go b/internal/featuretests/v3/httproute_test.go index 8a0c80ce6f6..35746900060 100644 --- a/internal/featuretests/v3/httproute_test.go +++ b/internal/featuretests/v3/httproute_test.go @@ -71,6 +71,7 @@ var ( Port: 443, Protocol: gatewayapi_v1beta1.HTTPSProtocolType, TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModeTerminate), CertificateRefs: []gatewayapi_v1beta1.SecretObjectReference{ gatewayapi.CertificateRef("tlscert", ""), }, @@ -117,10 +118,6 @@ func TestGateway_TLS(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "basic", Namespace: "default", - Labels: map[string]string{ - "app": "contour", - "type": "controller", - }, }, Spec: gatewayapi_v1beta1.HTTPRouteSpec{ CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ @@ -143,7 +140,7 @@ func TestGateway_TLS(t *testing.T) { c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ Resources: resources(t, - envoy_v3.RouteConfiguration("https/test.projectcontour.io", + envoy_v3.RouteConfiguration("http-80", envoy_v3.VirtualHost("test.projectcontour.io", &envoy_route_v3.Route{ Match: routeSegmentPrefix("/blog"), @@ -154,7 +151,7 @@ func TestGateway_TLS(t *testing.T) { }, ), ), - envoy_v3.RouteConfiguration("ingress_http", + envoy_v3.RouteConfiguration("https-443/test.projectcontour.io", envoy_v3.VirtualHost("test.projectcontour.io", &envoy_route_v3.Route{ Match: routeSegmentPrefix("/blog"), @@ -169,18 +166,18 @@ func TestGateway_TLS(t *testing.T) { TypeUrl: routeType, }) - c.Request(listenerType, "ingress_https").Equals(&envoy_discovery_v3.DiscoveryResponse{ + c.Request(listenerType, "https-443").Equals(&envoy_discovery_v3.DiscoveryResponse{ TypeUrl: listenerType, Resources: resources(t, &envoy_listener_v3.Listener{ - Name: "ingress_https", + Name: "https-443", Address: envoy_v3.SocketAddress("0.0.0.0", 8443), ListenerFilters: envoy_v3.ListenerFilters( envoy_v3.TLSInspector(), ), FilterChains: appendFilterChains( filterchaintls("test.projectcontour.io", sec1, - httpsFilterFor("test.projectcontour.io"), + httpsFilterForGateway("https-443", "test.projectcontour.io"), nil, "h2", "http/1.1"), ), SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), diff --git a/internal/featuretests/v3/routeweight_test.go b/internal/featuretests/v3/routeweight_test.go index 43eded31fc7..16770c995ff 100644 --- a/internal/featuretests/v3/routeweight_test.go +++ b/internal/featuretests/v3/routeweight_test.go @@ -25,6 +25,7 @@ import ( "github.com/projectcontour/contour/internal/fixture" "github.com/projectcontour/contour/internal/gatewayapi" "github.com/projectcontour/contour/internal/ref" + "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -397,14 +398,14 @@ func TestHTTPRoute_RouteWithAServiceWeight(t *testing.T) { rh.OnAdd(route1) - assertRDS(t, c, "1", virtualhosts( - envoy_v3.VirtualHost("test.projectcontour.io", + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, envoy_v3.RouteConfiguration("http-80", envoy_v3.VirtualHost("test.projectcontour.io", &envoy_route_v3.Route{ Match: routeSegmentPrefix("/blog"), Action: routecluster("default/svc1/80/da39a3ee5e"), }, - ), - ), nil) + ))), + }) // HTTPRoute with multiple weights. route2 := &gatewayapi_v1beta1.HTTPRoute{ @@ -432,16 +433,18 @@ func TestHTTPRoute_RouteWithAServiceWeight(t *testing.T) { } rh.OnUpdate(route1, route2) - assertRDS(t, c, "2", virtualhosts( - envoy_v3.VirtualHost("test.projectcontour.io", + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, envoy_v3.RouteConfiguration("http-80", envoy_v3.VirtualHost("test.projectcontour.io", &envoy_route_v3.Route{ Match: routeSegmentPrefix("/blog"), Action: routeWeightedCluster( weightedCluster{"default/svc1/80/da39a3ee5e", 60}, - weightedCluster{"default/svc2/80/da39a3ee5e", 90}), + weightedCluster{"default/svc2/80/da39a3ee5e", 90}, + ), }, - ), - ), nil) + ))), + }) } func TestTLSRoute_RouteWithAServiceWeight(t *testing.T) { @@ -515,11 +518,11 @@ func TestTLSRoute_RouteWithAServiceWeight(t *testing.T) { c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ Resources: resources(t, &envoy_listener_v3.Listener{ - Name: "ingress_https", + Name: "https-443", Address: envoy_v3.SocketAddress("0.0.0.0", 8443), FilterChains: []*envoy_listener_v3.FilterChain{{ Filters: envoy_v3.Filters( - tcpproxy("ingress_https", "default/svc1/443/da39a3ee5e"), + tcpproxy("https-443", "default/svc1/443/da39a3ee5e"), ), FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ ServerNames: []string{"test.projectcontour.io"}, @@ -535,13 +538,8 @@ func TestTLSRoute_RouteWithAServiceWeight(t *testing.T) { TypeUrl: listenerType, }) - // check that ingress_http is empty - c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ - Resources: resources(t, - envoy_v3.RouteConfiguration("ingress_http"), - ), - TypeUrl: routeType, - }) + // check that there is no route config + require.Empty(t, c.Request(routeType).Resources) // TLSRoute with multiple weighted services. route2 := &gatewayapi_v1alpha2.TLSRoute{ @@ -570,12 +568,12 @@ func TestTLSRoute_RouteWithAServiceWeight(t *testing.T) { c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ Resources: resources(t, &envoy_listener_v3.Listener{ - Name: "ingress_https", + Name: "https-443", Address: envoy_v3.SocketAddress("0.0.0.0", 8443), FilterChains: []*envoy_listener_v3.FilterChain{{ Filters: envoy_v3.Filters( tcpproxyWeighted( - "ingress_https", + "https-443", clusterWeight{name: "default/svc1/443/da39a3ee5e", weight: 1}, clusterWeight{name: "default/svc2/443/da39a3ee5e", weight: 7}, ), @@ -594,11 +592,6 @@ func TestTLSRoute_RouteWithAServiceWeight(t *testing.T) { TypeUrl: listenerType, }) - // check that ingress_http is empty - c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ - Resources: resources(t, - envoy_v3.RouteConfiguration("ingress_http"), - ), - TypeUrl: routeType, - }) + // check that there is no route config + require.Empty(t, c.Request(routeType).Resources) } diff --git a/internal/featuretests/v3/tlsroute_test.go b/internal/featuretests/v3/tlsroute_test.go index 04bb31eff08..94087ef7b7d 100644 --- a/internal/featuretests/v3/tlsroute_test.go +++ b/internal/featuretests/v3/tlsroute_test.go @@ -18,6 +18,7 @@ import ( "github.com/projectcontour/contour/internal/gatewayapi" "github.com/projectcontour/contour/internal/ref" + "github.com/stretchr/testify/require" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" @@ -99,11 +100,11 @@ func TestTLSRoute(t *testing.T) { c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ Resources: resources(t, &envoy_listener_v3.Listener{ - Name: "ingress_https", + Name: "https-443", Address: envoy_v3.SocketAddress("0.0.0.0", 8443), FilterChains: []*envoy_listener_v3.FilterChain{{ Filters: envoy_v3.Filters( - tcpproxy("ingress_https", "default/correct-backend/80/da39a3ee5e"), + tcpproxy("https-443", "default/correct-backend/80/da39a3ee5e"), ), FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ ServerNames: []string{"tcp.projectcontour.io"}, @@ -119,13 +120,8 @@ func TestTLSRoute(t *testing.T) { TypeUrl: listenerType, }) - // check that ingress_http is empty - c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ - Resources: resources(t, - envoy_v3.RouteConfiguration("ingress_http"), - ), - TypeUrl: routeType, - }) + // check that there is no route config + require.Empty(t, c.Request(routeType).Resources) // Route2 doesn't define any SNIs, so this should become the default backend. route2 := &gatewayapi_v1alpha2.TLSRoute{ @@ -147,11 +143,11 @@ func TestTLSRoute(t *testing.T) { c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ Resources: resources(t, &envoy_listener_v3.Listener{ - Name: "ingress_https", + Name: "https-443", Address: envoy_v3.SocketAddress("0.0.0.0", 8443), FilterChains: []*envoy_listener_v3.FilterChain{{ Filters: envoy_v3.Filters( - tcpproxy("ingress_https", "default/correct-backend/80/da39a3ee5e"), + tcpproxy("https-443", "default/correct-backend/80/da39a3ee5e"), ), FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ TransportProtocol: "tls", @@ -167,13 +163,8 @@ func TestTLSRoute(t *testing.T) { TypeUrl: listenerType, }) - // check that ingress_http is empty - c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ - Resources: resources(t, - envoy_v3.RouteConfiguration("ingress_http"), - ), - TypeUrl: routeType, - }) + // check that there is no route config + require.Empty(t, c.Request(routeType).Resources) route3 := &gatewayapi_v1alpha2.TLSRoute{ ObjectMeta: fixture.ObjectMeta("basic"), @@ -212,18 +203,18 @@ func TestTLSRoute(t *testing.T) { c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ Resources: resources(t, &envoy_listener_v3.Listener{ - Name: "ingress_https", + Name: "https-443", Address: envoy_v3.SocketAddress("0.0.0.0", 8443), FilterChains: []*envoy_listener_v3.FilterChain{{ Filters: envoy_v3.Filters( - tcpproxy("ingress_https", "default/correct-backend/80/da39a3ee5e"), + tcpproxy("https-443", "default/correct-backend/80/da39a3ee5e"), ), FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ ServerNames: []string{"tcp.projectcontour.io"}, }, }, { Filters: envoy_v3.Filters( - tcpproxy("ingress_https", "default/another-backend/80/da39a3ee5e"), + tcpproxy("https-443", "default/another-backend/80/da39a3ee5e"), ), FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ TransportProtocol: "tls", @@ -239,13 +230,8 @@ func TestTLSRoute(t *testing.T) { TypeUrl: listenerType, }) - // check that ingress_http is empty - c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ - Resources: resources(t, - envoy_v3.RouteConfiguration("ingress_http"), - ), - TypeUrl: routeType, - }) + // check that there is no route config + require.Empty(t, c.Request(routeType).Resources) rh.OnDelete(route1) rh.OnDelete(route2) diff --git a/internal/gatewayapi/listeners.go b/internal/gatewayapi/listeners.go index fecad1895db..247c0604d5e 100644 --- a/internal/gatewayapi/listeners.go +++ b/internal/gatewayapi/listeners.go @@ -18,144 +18,207 @@ import ( "net" "strings" + "github.com/projectcontour/contour/internal/ref" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) +// ContourHTTPSProtocolType is the protocol for an HTTPS Listener +// that is to be used with HTTPProxy/Ingress, where the TLS +// details are provided on the HTTPProxy/Ingress rather than +// on the Listener. +const ContourHTTPSProtocolType = "projectcontour.io/https" + type ValidateListenersResult struct { - InsecurePort int - SecurePort int + // ListenerNames is a map from Gateway Listener name + // to DAG/Envoy Listener name. All Gateway Listeners + // that share a port map to the same DAG/Envoy Listener + // name. + ListenerNames map[string]string + + // Ports is a list of ports to listen on. + Ports []ListenerPort + // InvalidListenerConditions is a map from Gateway Listener name + // to a condition to set, if the Listener is invalid. InvalidListenerConditions map[gatewayapi_v1beta1.SectionName]metav1.Condition } +type ListenerPort struct { + Name string + Port int32 + ContainerPort int32 + Protocol string +} + // ValidateListeners validates protocols, ports and hostnames on a set of listeners. // It ensures that: -// - all protocols are supported -// - each listener group (grouped by protocol, with HTTPS & TLS going together) uses a single port -// - listener hostnames are syntactically valid -// - hostnames within each listener group are unique +// - protocols are supported +// - hostnames are syntactically valid +// - listeners on each port have mutually compatible protocols +// - listeners on each port have unique hostnames // -// It returns the insecure & secure ports to use, as well as conditions for all invalid listeners. +// It returns a Listener name map, the ports to use, and conditions for all invalid listeners. // If a listener is not in the "InvalidListenerConditions" map, it is assumed to be valid according // to the above rules. func ValidateListeners(listeners []gatewayapi_v1beta1.Listener) ValidateListenersResult { - result := ValidateListenersResult{ - InvalidListenerConditions: map[gatewayapi_v1beta1.SectionName]metav1.Condition{}, - } - - // All listeners with a protocol of "HTTP" must use the same port number - // Heuristic: the first port number encountered is allowed, any other listeners with a different port number are marked "Detached" with "PortUnavailable" - // All listeners with a protocol of "HTTP" using the one allowed port must have a unique hostname - // Any listener with a duplicate hostname is marked "Conflicted" with "HostnameConflict" - - var ( - insecureHostnames = map[string]int{} - secureHostnames = map[string]int{} + // TLS-based protocols that can all exist on the same port. + compatibleTLSProtocols := sets.New( + gatewayapi_v1beta1.HTTPSProtocolType, + gatewayapi_v1beta1.TLSProtocolType, + ContourHTTPSProtocolType, ) - for _, listener := range listeners { - hostname := HostnameDeref(listener.Hostname) - - switch listener.Protocol { - case gatewayapi_v1beta1.HTTPProtocolType: - // Keep the first insecure listener port we see - if result.InsecurePort == 0 { - result.InsecurePort = int(listener.Port) - } - - // Count hostnames among insecure listeners with the "valid" port. - // For other insecure listeners with an "invalid" port, the - // "PortUnavailable" reason will take precedence. - if int(listener.Port) == result.InsecurePort { - insecureHostnames[hostname]++ - } - case gatewayapi_v1beta1.HTTPSProtocolType, gatewayapi_v1beta1.TLSProtocolType: - // Keep the first secure listener port we see - if result.SecurePort == 0 { - result.SecurePort = int(listener.Port) - } - - // Count hostnames among secure listeners with the "valid" port. - // For other secure listeners with an "invalid" port, the - // "PortUnavailable" reason will take precedence. - if int(listener.Port) == result.SecurePort { - secureHostnames[hostname]++ - } - } + result := ValidateListenersResult{ + ListenerNames: map[string]string{}, + InvalidListenerConditions: map[gatewayapi_v1beta1.SectionName]metav1.Condition{}, } - for _, listener := range listeners { - hostname := HostnameDeref(listener.Hostname) - - if len(hostname) > 0 { - if err := IsValidHostname(hostname); err != nil { + for i, listener := range listeners { + // Check for a valid hostname. + if hostname := ref.Val(listener.Hostname, ""); len(hostname) > 0 { + if err := IsValidHostname(string(hostname)); err != nil { result.InvalidListenerConditions[listener.Name] = metav1.Condition{ Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), Status: metav1.ConditionFalse, Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), Message: err.Error(), } + continue } } + // Check for a supported protocol. switch listener.Protocol { - case gatewayapi_v1beta1.HTTPProtocolType: - switch { - case int(listener.Port) != result.InsecurePort: - result.InvalidListenerConditions[listener.Name] = metav1.Condition{ - Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), - Message: "Only one HTTP port is supported", - } - case insecureHostnames[hostname] > 1: - result.InvalidListenerConditions[listener.Name] = metav1.Condition{ - Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), - Status: metav1.ConditionTrue, - Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), - Message: "Hostname must be unique among HTTP listeners", - } - } - case gatewayapi_v1beta1.HTTPSProtocolType, gatewayapi_v1beta1.TLSProtocolType: - switch { - case int(listener.Port) != result.SecurePort: - result.InvalidListenerConditions[listener.Name] = metav1.Condition{ - Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), - Message: "Only one HTTPS/TLS port is supported", - } - case secureHostnames[hostname] > 1: - result.InvalidListenerConditions[listener.Name] = metav1.Condition{ - Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), - Status: metav1.ConditionTrue, - Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), - Message: "Hostname must be unique among HTTPS/TLS listeners", - } - } + case gatewayapi_v1beta1.HTTPProtocolType, gatewayapi_v1beta1.HTTPSProtocolType, gatewayapi_v1beta1.TLSProtocolType, ContourHTTPSProtocolType: default: result.InvalidListenerConditions[listener.Name] = metav1.Condition{ Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), Status: metav1.ConditionFalse, Reason: string(gatewayapi_v1beta1.ListenerReasonUnsupportedProtocol), - Message: fmt.Sprintf("Listener protocol %q is unsupported, must be one of HTTP, HTTPS or TLS", listener.Protocol), + Message: fmt.Sprintf("Listener protocol %q is unsupported, must be one of HTTP, HTTPS, TLS or projectcontour.io/https", listener.Protocol), } + continue + } + + conflicted := func() bool { + // Check for conflicts with previous Listeners only. + // This allows Listeners that appear first in list + // order to take precedence, i.e. to be accepted and + // programmed, when there is a conflict. + for j := 0; j < i; j++ { + otherListener := listeners[j] + + if listener.Port != otherListener.Port { + // Port ranges 57536-58558 and 58559-59581 both map to container ports + // 1024-2046, since we can't listen on ports 1-1023 in the Envoy container. + // If there are conflicting container ports, the listener can't be accepted. + if toContainerPort(listener.Port) == toContainerPort(otherListener.Port) { + result.InvalidListenerConditions[listener.Name] = metav1.Condition{ + Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), + Message: "Listener port conflicts with a previous Listener's port", + } + return true + } + + // Otherwise, listeners on different ports can't conflict. + continue + } + + // Protocol conflict + if listener.Protocol == gatewayapi_v1beta1.HTTPProtocolType { + if otherListener.Protocol != gatewayapi_v1beta1.HTTPProtocolType { + result.InvalidListenerConditions[listener.Name] = metav1.Condition{ + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonProtocolConflict), + Message: "All Listener protocols for a given port must be compatible", + } + return true + } + } else if compatibleTLSProtocols.Has(listener.Protocol) { + if !compatibleTLSProtocols.Has(otherListener.Protocol) { + result.InvalidListenerConditions[listener.Name] = metav1.Condition{ + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonProtocolConflict), + Message: "All Listener protocols for a given port must be compatible", + } + return true + } + } + + // Hostname conflict + if ref.Val(listener.Hostname, "") == ref.Val(otherListener.Hostname, "") { + result.InvalidListenerConditions[listener.Name] = metav1.Condition{ + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), + Message: "All Listener hostnames for a given port must be unique", + } + return true + } + } + + return false + }() + + if conflicted { + continue + } + + // Add an entry in the Listener name map. + var protocol string + if listener.Protocol == gatewayapi_v1beta1.HTTPProtocolType { + protocol = "http" + } else { + protocol = "https" + } + envoyListenerName := fmt.Sprintf("%s-%d", protocol, listener.Port) + + result.ListenerNames[string(listener.Name)] = envoyListenerName + + // Add the port to the list if it hasn't been added already. + found := false + for _, port := range result.Ports { + if port.Name == envoyListenerName { + found = true + break + } + } + + if !found { + result.Ports = append(result.Ports, ListenerPort{ + Name: envoyListenerName, + Port: int32(listener.Port), + ContainerPort: toContainerPort(listener.Port), + Protocol: protocol, + }) } } return result } -// HostnameDeref returns the hostname as a string if it's not nil, -// or an empty string otherwise. -func HostnameDeref(hostname *gatewayapi_v1beta1.Hostname) string { - if hostname == nil { - return "" +func toContainerPort(listenerPort gatewayapi_v1beta1.PortNumber) int32 { + // Add 8000 to the Listener port, wrapping around if needed, + // and skipping over privileged ports 1-1023. + + containerPort := listenerPort + 8000 + + if containerPort > 65535 { + containerPort -= 65535 + } + + if containerPort <= 1023 { + containerPort += 1023 } - return string(*hostname) + return int32(containerPort) } // IsValidHostname validates that a given hostname is syntactically valid. diff --git a/internal/gatewayapi/listeners_test.go b/internal/gatewayapi/listeners_test.go index 636c99c23d5..d8a2e7539a4 100644 --- a/internal/gatewayapi/listeners_test.go +++ b/internal/gatewayapi/listeners_test.go @@ -23,339 +23,529 @@ import ( ) func TestValidateListeners(t *testing.T) { - // All HTTP listeners are valid, some non-HTTP listeners - // as well. - listeners := []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), - }, - { - Name: "listener-4", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), - }, - { - Name: "non-http-listener-1", - Protocol: gatewayapi_v1beta1.TLSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - } + t.Run("All HTTP listeners are valid on a single port, some non-HTTP listeners as well", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), + }, + { + Name: "listener-4", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), + }, + { + Name: "non-http-listener-1", + Protocol: gatewayapi_v1beta1.TLSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + } - res := ValidateListeners(listeners) - assert.Equal(t, 80, res.InsecurePort) - assert.Empty(t, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-80", Port: 80, ContainerPort: 8080, Protocol: "http"}, + {Name: "https-443", Port: 443, ContainerPort: 8443, Protocol: "https"}, + }) + assert.Empty(t, res.InvalidListenerConditions) + }) - // One HTTP listener with an invalid port number, some - // non-HTTP listeners as well. - listeners = []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), - }, - { - Name: "listener-4", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 8080, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - { - Name: "non-http-listener-1", - Protocol: gatewayapi_v1beta1.TLSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - } + t.Run("HTTP listeners on multiple ports, some non-HTTP listeners as well", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), + }, + { + Name: "listener-4", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 8080, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "non-http-listener-1", + Protocol: gatewayapi_v1beta1.TLSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + } - res = ValidateListeners(listeners) - assert.Equal(t, 80, res.InsecurePort) - assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ - "listener-4": { - Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), - Message: "Only one HTTP port is supported", - }, - }, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-80", Port: 80, ContainerPort: 8080, Protocol: "http"}, + {Name: "http-8080", Port: 8080, ContainerPort: 16080, Protocol: "http"}, + {Name: "https-443", Port: 443, ContainerPort: 8443, Protocol: "https"}, + }) + assert.Empty(t, res.InvalidListenerConditions) + }) - // Two HTTP listeners with the same hostname, some HTTP - // listeners with invalid port, some non-HTTP listeners as well. - listeners = []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), // duplicate hostname - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), // duplicate hostname - }, - { - Name: "listener-4", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), - }, - { - Name: "listener-5", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 8080, // invalid port - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), - }, - { - Name: "non-http-listener-1", - Protocol: gatewayapi_v1beta1.TLSProtocolType, // non-HTTP - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - } + t.Run("Two HTTP listeners with the same hostname, some HTTP listeners on another port, some non-HTTP listeners as well", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), // duplicate hostname + }, + { + Name: "listener-4", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), + }, + { + Name: "listener-5", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 8080, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), + }, + { + Name: "non-http-listener-1", + Protocol: gatewayapi_v1beta1.TLSProtocolType, // non-HTTP + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + } - res = ValidateListeners(listeners) - assert.Equal(t, 80, res.InsecurePort) - assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ - "listener-2": { - Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), - Status: metav1.ConditionTrue, - Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), - Message: "Hostname must be unique among HTTP listeners", - }, - "listener-3": { - Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), - Status: metav1.ConditionTrue, - Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), - Message: "Hostname must be unique among HTTP listeners", - }, - "listener-5": { - Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), - Message: "Only one HTTP port is supported", - }, - }, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-80", Port: 80, ContainerPort: 8080, Protocol: "http"}, + {Name: "http-8080", Port: 8080, ContainerPort: 16080, Protocol: "http"}, + {Name: "https-443", Port: 443, ContainerPort: 8443, Protocol: "https"}, + }) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "listener-3": { + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), + Message: "All Listener hostnames for a given port must be unique", + }, + }, res.InvalidListenerConditions) + }) - // All HTTPS/TLS listeners are valid, some non-HTTPS/TLS listeners - // as well. - listeners = []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.TLSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), - }, - { - Name: "listener-4", - Protocol: gatewayapi_v1beta1.TLSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), - }, - { - Name: "non-http-listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - } + t.Run("All HTTPS/TLS listeners are valid, some non-HTTPS/TLS listeners as well", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.TLSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), + }, + { + Name: "listener-4", + Protocol: gatewayapi_v1beta1.TLSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), + }, + { + Name: "non-http-listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + } - res = ValidateListeners(listeners) - assert.Equal(t, 443, res.SecurePort) - assert.Empty(t, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-80", Port: 80, ContainerPort: 8080, Protocol: "http"}, + {Name: "https-443", Port: 443, ContainerPort: 8443, Protocol: "https"}, + }) + assert.Empty(t, res.InvalidListenerConditions) + }) - // One HTTPS listener with an invalid port number, some - // non-HTTPS listeners as well. - listeners = []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.TLSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), - }, - { - Name: "listener-4", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 8443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - { - Name: "http-listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - } + t.Run("HTTPS listeners on two different ports, some non-HTTPS listeners as well", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.TLSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), + }, + { + Name: "listener-4", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 8443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "http-listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + } - res = ValidateListeners(listeners) - assert.Equal(t, 443, res.SecurePort) - assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ - "listener-4": { - Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), - Message: "Only one HTTPS/TLS port is supported", - }, - }, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-80", Port: 80, ContainerPort: 8080, Protocol: "http"}, + {Name: "https-443", Port: 443, ContainerPort: 8443, Protocol: "https"}, + {Name: "https-8443", Port: 8443, ContainerPort: 16443, Protocol: "https"}, + }) + assert.Empty(t, res.InvalidListenerConditions) + }) - // Two HTTPS/TLS listeners with the same hostname, some HTTPS/TLS - // listeners with invalid port, some HTTP listeners as well. - listeners = []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), // duplicate hostname - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.TLSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), // duplicate hostname - }, - { - Name: "listener-4", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), - }, - { - Name: "listener-5", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 8443, // invalid port - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), - }, - { - Name: "http-listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), - }, - } + t.Run("Two HTTPS/TLS listeners on same port with the same hostname, some HTTPS/TLS listeners on another port, some HTTP listeners as well", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.TLSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), // duplicate hostname + }, + { + Name: "listener-4", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), + }, + { + Name: "listener-5", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 8443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.envoyproxy.io")), + }, + { + Name: "http-listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("local.projectcontour.io")), + }, + } - res = ValidateListeners(listeners) - assert.Equal(t, 443, res.SecurePort) - assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ - "listener-2": { - Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), - Status: metav1.ConditionTrue, - Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), - Message: "Hostname must be unique among HTTPS/TLS listeners", - }, - "listener-3": { - Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), - Status: metav1.ConditionTrue, - Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), - Message: "Hostname must be unique among HTTPS/TLS listeners", - }, - "listener-5": { - Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), - Message: "Only one HTTPS/TLS port is supported", - }, - }, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-80", Port: 80, ContainerPort: 8080, Protocol: "http"}, + {Name: "https-443", Port: 443, ContainerPort: 8443, Protocol: "https"}, + {Name: "https-8443", Port: 8443, ContainerPort: 16443, Protocol: "https"}, + }) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "listener-3": { + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonHostnameConflict), + Message: "All Listener hostnames for a given port must be unique", + }, + }, res.InvalidListenerConditions) + }) - // Two HTTP and one HTTPS listeners, each with an invalid hostname. - listeners = []gatewayapi_v1beta1.Listener{ - { - Name: "listener-1", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("192.168.1.1")), - }, - { - Name: "listener-2", - Protocol: gatewayapi_v1beta1.HTTPProtocolType, - Port: 80, - Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.*.projectcontour.io")), - }, - { - Name: "listener-3", - Protocol: gatewayapi_v1beta1.HTTPSProtocolType, - Port: 443, - Hostname: ref.To(gatewayapi_v1beta1.Hostname(".invalid.$.")), - }, - } + t.Run("Two HTTP and one HTTPS listeners, each with an invalid hostname", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "listener-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("192.168.1.1")), + }, + { + Name: "listener-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 80, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.*.projectcontour.io")), + }, + { + Name: "listener-3", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 443, + Hostname: ref.To(gatewayapi_v1beta1.Hostname(".invalid.$.")), + }, + } - res = ValidateListeners(listeners) - assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ - "listener-1": { - Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), - Message: "invalid hostname \"192.168.1.1\": must be a DNS name, not an IP address", - }, - "listener-2": { - Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), - Message: "invalid hostname \"*.*.projectcontour.io\": [a wildcard DNS-1123 subdomain must start with '*.', followed by a valid DNS subdomain, which must consist of lower case alphanumeric characters, '-' or '.' and end with an alphanumeric character (e.g. '*.example.com', regex used for validation is '\\*\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", - }, - "listener-3": { - Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), - Message: "invalid hostname \".invalid.$.\": [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", - }, - }, res.InvalidListenerConditions) + res := ValidateListeners(listeners) + assert.Empty(t, res.Ports) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "listener-1": { + Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), + Message: "invalid hostname \"192.168.1.1\": must be a DNS name, not an IP address", + }, + "listener-2": { + Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), + Message: "invalid hostname \"*.*.projectcontour.io\": [a wildcard DNS-1123 subdomain must start with '*.', followed by a valid DNS subdomain, which must consist of lower case alphanumeric characters, '-' or '.' and end with an alphanumeric character (e.g. '*.example.com', regex used for validation is '\\*\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + }, + "listener-3": { + Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), + Message: "invalid hostname \".invalid.$.\": [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + }, + }, res.InvalidListenerConditions) + }) + + t.Run("Three HTTPS listeners on the same port, each with a different hostname", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "https-1", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("https-1.gateway.projectcontour.io")), + Port: 443, + }, + { + Name: "https-2", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("https-2.gateway.projectcontour.io")), + Port: 443, + }, + { + Name: "https-3", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("https-3.gateway.projectcontour.io")), + Port: 443, + }, + } + + res := ValidateListeners(listeners) + assert.Len(t, res.InvalidListenerConditions, 0) + assert.Len(t, res.Ports, 1) + assert.Len(t, res.ListenerNames, 3) + }) + + t.Run("Conflicting protocols on a port", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "http", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 7777, + }, + { + Name: "https", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 7777, + }, + + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 9999, + }, + { + Name: "projectcontour-io-https", + Protocol: ContourHTTPSProtocolType, + Port: 9999, + }, + } + + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-7777", Port: 7777, ContainerPort: 15777, Protocol: "http"}, + {Name: "http-9999", Port: 9999, ContainerPort: 17999, Protocol: "http"}, + }) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "https": { + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonProtocolConflict), + Message: "All Listener protocols for a given port must be compatible", + }, + "projectcontour-io-https": { + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonProtocolConflict), + Message: "All Listener protocols for a given port must be compatible", + }, + }, res.InvalidListenerConditions) + }) + + t.Run("Conflicting protocols on a port (reverse order)", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "https", + Protocol: gatewayapi_v1beta1.HTTPSProtocolType, + Port: 7777, + }, + { + Name: "http", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 7777, + }, + + { + Name: "projectcontour-io-https", + Protocol: ContourHTTPSProtocolType, + Port: 9999, + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 9999, + }, + } + + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "https-7777", Port: 7777, ContainerPort: 15777, Protocol: "https"}, + {Name: "https-9999", Port: 9999, ContainerPort: 17999, Protocol: "https"}, + }) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "http": { + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonProtocolConflict), + Message: "All Listener protocols for a given port must be compatible", + }, + "http-2": { + Type: string(gatewayapi_v1beta1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonProtocolConflict), + Message: "All Listener protocols for a given port must be compatible", + }, + }, res.InvalidListenerConditions) + }) + + t.Run("Listeners with various edge-case port numbers", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 65535, // wraps around, does not hit a privileged port + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 57536, // wraps around, hits a privileged port + }, + { + Name: "http-3", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 58560, // wraps around, does not hit a privileged port + }, + } + + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-65535", Port: 65535, ContainerPort: 8000, Protocol: "http"}, + {Name: "http-57536", Port: 57536, ContainerPort: 1024, Protocol: "http"}, + {Name: "http-58560", Port: 58560, ContainerPort: 1025, Protocol: "http"}, + }) + assert.Empty(t, res.InvalidListenerConditions) + }) + + t.Run("Listeners with ports that map to the same container ports", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 58000, + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 59023, + }, + } + + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-58000", Port: 58000, ContainerPort: 1488, Protocol: "http"}, + }) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "http-2": { + Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), + Message: "Listener port conflicts with a previous Listener's port", + }, + }, res.InvalidListenerConditions) + }) + + t.Run("Listeners with ports that map to the same container ports, reverse order", func(t *testing.T) { + listeners := []gatewayapi_v1beta1.Listener{ + { + Name: "http-1", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 59000, + }, + { + Name: "http-2", + Protocol: gatewayapi_v1beta1.HTTPProtocolType, + Port: 57977, + }, + } + + res := ValidateListeners(listeners) + assert.ElementsMatch(t, res.Ports, []ListenerPort{ + {Name: "http-59000", Port: 59000, ContainerPort: 1465, Protocol: "http"}, + }) + assert.Equal(t, map[gatewayapi_v1beta1.SectionName]metav1.Condition{ + "http-2": { + Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.ListenerReasonPortUnavailable), + Message: "Listener port conflicts with a previous Listener's port", + }, + }, res.InvalidListenerConditions) + }) } diff --git a/internal/provisioner/controller/gateway.go b/internal/provisioner/controller/gateway.go index ae4668ba6c8..d7f487520ea 100644 --- a/internal/provisioner/controller/gateway.go +++ b/internal/provisioner/controller/gateway.go @@ -208,23 +208,12 @@ func (r *gatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // Validate listener ports and hostnames to get // the ports to program. - validateListenersResult := gatewayapi.ValidateListeners(gateway.Spec.Listeners) - - if validateListenersResult.InsecurePort > 0 { - port := model.Port{ - Name: "http", - ServicePort: int32(validateListenersResult.InsecurePort), - ContainerPort: 8080, - } - contourModel.Spec.NetworkPublishing.Envoy.Ports = append(contourModel.Spec.NetworkPublishing.Envoy.Ports, port) - } - if validateListenersResult.SecurePort > 0 { - port := model.Port{ - Name: "https", - ServicePort: int32(validateListenersResult.SecurePort), - ContainerPort: 8443, - } - contourModel.Spec.NetworkPublishing.Envoy.Ports = append(contourModel.Spec.NetworkPublishing.Envoy.Ports, port) + for _, listenerPort := range gatewayapi.ValidateListeners(gateway.Spec.Listeners).Ports { + contourModel.Spec.NetworkPublishing.Envoy.Ports = append(contourModel.Spec.NetworkPublishing.Envoy.Ports, model.Port{ + Name: listenerPort.Name, + ServicePort: listenerPort.Port, + ContainerPort: listenerPort.ContainerPort, + }) } gatewayClassParams, err := r.getGatewayClassParams(ctx, gatewayClass) @@ -311,18 +300,10 @@ func (r *gatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if networkPublishing.Type == contour_api_v1alpha1.NodePortServicePublishingType { // when the NetworkPublishingType is 'NodePortServicePublishingType', - // the gateway.Spec.Listeners' port will be used to set 'NodePort' NOT 'ServicePort' - // in this scenario, the service port values will be reassigned with 80/443. + // the gateway.Spec.Listeners' port will be used to set 'NodePort' in addition to 'ServicePort' for i := range contourModel.Spec.NetworkPublishing.Envoy.Ports { port := &contourModel.Spec.NetworkPublishing.Envoy.Ports[i] - switch port.Name { - case "http": - port.NodePort = port.ServicePort - port.ServicePort = 80 - case "https": - port.NodePort = port.ServicePort - port.ServicePort = 443 - } + port.NodePort = port.ServicePort } } diff --git a/internal/provisioner/controller/gateway_test.go b/internal/provisioner/controller/gateway_test.go index b49fe2312fa..578cdfe6760 100644 --- a/internal/provisioner/controller/gateway_test.go +++ b/internal/provisioner/controller/gateway_test.go @@ -477,19 +477,18 @@ func TestGatewayReconcile(t *testing.T) { { Name: "listener-1", Protocol: gatewayv1beta1.HTTPProtocolType, - Port: 82, + Port: 80, }, { Name: "listener-2", Protocol: gatewayv1beta1.HTTPProtocolType, - Port: 82, + Port: 80, Hostname: ref.To(gatewayv1beta1.Hostname("foo.bar")), }, - // listener-3's port will be ignored because it's different than the previous HTTP listeners' { Name: "listener-3", Protocol: gatewayv1beta1.HTTPProtocolType, - Port: 80, + Port: 81, }, // listener-4 will be ignored because it's an unsupported protocol { @@ -500,19 +499,18 @@ func TestGatewayReconcile(t *testing.T) { { Name: "listener-5", Protocol: gatewayv1beta1.HTTPSProtocolType, - Port: 8443, + Port: 443, }, { Name: "listener-6", Protocol: gatewayv1beta1.TLSProtocolType, - Port: 8443, + Port: 443, Hostname: ref.To(gatewayv1beta1.Hostname("foo.bar")), }, - // listener-7's port will be ignored because it's different than the previous HTTPS/TLS listeners' { Name: "listener-7", Protocol: gatewayv1beta1.HTTPSProtocolType, - Port: 8444, + Port: 8443, Hostname: ref.To(gatewayv1beta1.Hostname("foo.baz")), }, }), @@ -528,19 +526,31 @@ func TestGatewayReconcile(t *testing.T) { } require.NoError(t, r.client.Get(context.Background(), keyFor(envoyService), envoyService)) - require.Len(t, envoyService.Spec.Ports, 2) + require.Len(t, envoyService.Spec.Ports, 4) assert.Contains(t, envoyService.Spec.Ports, corev1.ServicePort{ - Name: "http", + Name: "http-80", Protocol: corev1.ProtocolTCP, - Port: 82, + Port: 80, TargetPort: intstr.IntOrString{IntVal: 8080}, }) assert.Contains(t, envoyService.Spec.Ports, corev1.ServicePort{ - Name: "https", + Name: "http-81", Protocol: corev1.ProtocolTCP, - Port: 8443, + Port: 81, + TargetPort: intstr.IntOrString{IntVal: 8081}, + }) + assert.Contains(t, envoyService.Spec.Ports, corev1.ServicePort{ + Name: "https-443", + Protocol: corev1.ProtocolTCP, + Port: 443, TargetPort: intstr.IntOrString{IntVal: 8443}, }) + assert.Contains(t, envoyService.Spec.Ports, corev1.ServicePort{ + Name: "https-8443", + Protocol: corev1.ProtocolTCP, + Port: 8443, + TargetPort: intstr.IntOrString{IntVal: 16443}, + }) }, }, "The Envoy service's ports are derived from the Gateway's listeners (http only)": { @@ -549,19 +559,18 @@ func TestGatewayReconcile(t *testing.T) { { Name: "listener-1", Protocol: gatewayv1beta1.HTTPProtocolType, - Port: 82, + Port: 80, }, { Name: "listener-2", Protocol: gatewayv1beta1.HTTPProtocolType, - Port: 82, + Port: 80, Hostname: ref.To(gatewayv1beta1.Hostname("foo.bar")), }, - // listener-3's port will be ignored because it's different than the previous HTTP listeners' { Name: "listener-3", Protocol: gatewayv1beta1.HTTPProtocolType, - Port: 80, + Port: 8080, }, // listener-4 will be ignored because it's an unsupported protocol { @@ -581,13 +590,19 @@ func TestGatewayReconcile(t *testing.T) { } require.NoError(t, r.client.Get(context.Background(), keyFor(envoyService), envoyService)) - require.Len(t, envoyService.Spec.Ports, 1) + require.Len(t, envoyService.Spec.Ports, 2) assert.Contains(t, envoyService.Spec.Ports, corev1.ServicePort{ - Name: "http", + Name: "http-80", Protocol: corev1.ProtocolTCP, - Port: 82, + Port: 80, TargetPort: intstr.IntOrString{IntVal: 8080}, }) + assert.Contains(t, envoyService.Spec.Ports, corev1.ServicePort{ + Name: "http-8080", + Protocol: corev1.ProtocolTCP, + Port: 8080, + TargetPort: intstr.IntOrString{IntVal: 16080}, + }) }, }, "If ContourDeployment.Spec.Contour.Replicas is not specified, the Contour deployment defaults to 2 replicas": { @@ -940,9 +955,9 @@ func TestGatewayReconcile(t *testing.T) { assert.Len(t, svc.Spec.Ports, 2) assert.Equal(t, int32(30000), svc.Spec.Ports[0].NodePort) - assert.Equal(t, int32(80), svc.Spec.Ports[0].Port) + assert.Equal(t, int32(30000), svc.Spec.Ports[0].Port) assert.Equal(t, int32(30001), svc.Spec.Ports[1].NodePort) - assert.Equal(t, int32(443), svc.Spec.Ports[1].Port) + assert.Equal(t, int32(30001), svc.Spec.Ports[1].Port) }, }, "If ContourDeployment.Spec.Envoy.WorkloadType is set to Deployment, an Envoy deployment is provisioned with the specified number of replicas": { diff --git a/internal/provisioner/objects/dataplane/dataplane.go b/internal/provisioner/objects/dataplane/dataplane.go index 78445b29181..56792353b8f 100644 --- a/internal/provisioner/objects/dataplane/dataplane.go +++ b/internal/provisioner/objects/dataplane/dataplane.go @@ -126,16 +126,6 @@ func EnsureDataPlaneDeleted(ctx context.Context, cli client.Client, contour *mod } func desiredContainers(contour *model.Contour, contourImage, envoyImage string) ([]corev1.Container, []corev1.Container) { - var ports []corev1.ContainerPort - for _, port := range contour.Spec.NetworkPublishing.Envoy.Ports { - p := corev1.ContainerPort{ - Name: port.Name, - ContainerPort: port.ContainerPort, - Protocol: corev1.ProtocolTCP, - } - ports = append(ports, p) - } - var ( metricsPort = objects.EnvoyMetricsPort healthPort = objects.EnvoyHealthPort @@ -156,11 +146,11 @@ func desiredContainers(contour *model.Contour, contourImage, envoyImage string) } } - ports = append(ports, corev1.ContainerPort{ + ports := []corev1.ContainerPort{{ Name: "metrics", ContainerPort: metricsPort, Protocol: corev1.ProtocolTCP, - }) + }} containers := []corev1.Container{ { diff --git a/internal/provisioner/objects/dataplane/dataplane_test.go b/internal/provisioner/objects/dataplane/dataplane_test.go index 54544a42af0..3cb90e9cdbc 100644 --- a/internal/provisioner/objects/dataplane/dataplane_test.go +++ b/internal/provisioner/objects/dataplane/dataplane_test.go @@ -317,9 +317,6 @@ func TestDesiredDaemonSet(t *testing.T) { checkDaemonSetHasEnvVar(t, ds, EnvoyContainerName, envoyPodEnvVar) checkDaemonSetHasEnvVar(t, ds, envoyInitContainerName, envoyNsEnvVar) checkDaemonSetHasLabels(t, ds, cntr.AppLabels()) - for _, port := range cntr.Spec.NetworkPublishing.Envoy.Ports { - checkContainerHasPort(t, ds, port.ContainerPort) - } checkContainerHasPort(t, ds, int32(cntr.Spec.RuntimeSettings.Envoy.Metrics.Port)) checkDaemonSetHasNodeSelector(t, ds, nil) diff --git a/test/conformance/gatewayapi/gateway_conformance_test.go b/test/conformance/gatewayapi/gateway_conformance_test.go index 96f5d336550..e06e499b39d 100644 --- a/test/conformance/gatewayapi/gateway_conformance_test.go +++ b/test/conformance/gatewayapi/gateway_conformance_test.go @@ -60,11 +60,6 @@ func TestGatewayConformance(t *testing.T) { // Keep the list of skipped features in sync with // test/scripts/run-gateway-conformance.sh. SkipTests: []string{ - // Test adds multiple HTTP Listeners to the Gateway with - // distinct ports. This is not supported in Contour until - // multi-Listener support is added. - // See: https://github.com/projectcontour/contour/issues/4960 - tests.GatewayObservedGenerationBump.ShortName, // Checks for the original request port in the returned Location // header which Envoy is stripping. // See: https://github.com/envoyproxy/envoy/issues/17318