From 8e175b72af16196075b7460b3851982699891f0d Mon Sep 17 00:00:00 2001 From: Steve Sloka Date: Wed, 28 Apr 2021 11:30:27 -0400 Subject: [PATCH] internal: Adds support for TLSRoute Add support for TLSRoute to enable Passthrough TCP Proxying to pods via SNI. Updates #3440 Signed-off-by: Steve Sloka --- .../fixtures/ingress-conformance-echo.yaml | 4 + .../testsuite/gatewayapi/008-tlsroute.yaml | 188 ++++++++++++ internal/dag/builder_test.go | 258 ++++++++++++++++ internal/dag/cache.go | 7 - internal/dag/gatewayapi_processor.go | 197 +++++++++++-- internal/dag/gatewayapi_processor_test.go | 2 +- internal/dag/status_test.go | 278 +++++++++++++++++- internal/envoy/v3/listener.go | 15 +- internal/envoy/v3/listener_test.go | 35 +++ internal/featuretests/v3/tlsroute_test.go | 169 +++++++++++ internal/status/cache.go | 27 +- .../{httproutestatus.go => conditions.go} | 78 +++-- ...routestatus_test.go => conditions_test.go} | 2 +- 13 files changed, 1173 insertions(+), 87 deletions(-) create mode 100644 _integration/testsuite/gatewayapi/008-tlsroute.yaml create mode 100644 internal/featuretests/v3/tlsroute_test.go rename internal/status/{httproutestatus.go => conditions.go} (70%) rename internal/status/{httproutestatus_test.go => conditions_test.go} (98%) diff --git a/_integration/testsuite/fixtures/ingress-conformance-echo.yaml b/_integration/testsuite/fixtures/ingress-conformance-echo.yaml index 9e977687337..07b1888e4ff 100644 --- a/_integration/testsuite/fixtures/ingress-conformance-echo.yaml +++ b/_integration/testsuite/fixtures/ingress-conformance-echo.yaml @@ -95,6 +95,10 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + - name: INGRESS_NAME + value: *name + - name: SERVICE_NAME + value: *name - name: TLS_SERVER_CERT value: /run/secrets/certs/tls.crt - name: TLS_SERVER_PRIVKEY diff --git a/_integration/testsuite/gatewayapi/008-tlsroute.yaml b/_integration/testsuite/gatewayapi/008-tlsroute.yaml new file mode 100644 index 00000000000..97de51cee9b --- /dev/null +++ b/_integration/testsuite/gatewayapi/008-tlsroute.yaml @@ -0,0 +1,188 @@ +# Copyright Project Contour Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + + import data.contour.resources + + # Ensure that cert-manager is installed. + # Version check the certificates resource. + + Group := "cert-manager.io" + Version := "v1" + + have_certmanager_version { + v := resources.versions["certificates"] + v[_].Group == Group + v[_].Version == Version +} + + skip[msg] { + not resources.is_supported("certificates") + msg := "cert-manager is not installed" +} + + skip[msg] { + not have_certmanager_version + + avail := resources.versions["certificates"] + + msg := concat("\n", [ + sprintf("cert-manager version %s/%s is not installed", [Group, Version]), + "available versions:", + yaml.marshal(avail) + ]) +} + +--- + +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned +spec: + selfSigned: {} + +--- + +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: backend-server-cert +spec: + dnsNames: + - tcp.projectcontour.io + secretName: backend-server-cert + issuerRef: + name: selfsigned + kind: ClusterIssuer + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-conformance-echo-tls +$apply: + fixture: + as: echo-slash-blue + +--- + +apiVersion: v1 +kind: Service +metadata: + name: ingress-conformance-echo-tls +$apply: + fixture: + as: echo-slash-blue + +--- + +apiVersion: networking.x-k8s.io/v1alpha1 +kind: GatewayClass +metadata: + name: contour-class +spec: + controller: projectcontour.io/ingress-controller + +--- + +apiVersion: networking.x-k8s.io/v1alpha1 +kind: Gateway +metadata: + name: contour + namespace: projectcontour +spec: + gatewayClassName: contour-class + listeners: + - protocol: TLS + port: 443 + routes: + kind: TLSRoute + namespaces: + from: "All" +--- + +apiVersion: networking.x-k8s.io/v1alpha1 +kind: TLSRoute +metadata: + name: tlsroute1 +spec: + rules: + - matches: + - snis: + - tcp.projectcontour.io + forwardTo: + - serviceName: echo-slash-blue + port: 443 +--- + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + +# Ensure / request returns 200 status code. +Response := client.Get({ + "url": url.https("/"), + "headers": { + "Host": "tcp.projectcontour.io", + "User-Agent": client.ua("tlsroute"), + }, + "tls_insecure_skip_verify": true, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 200) +} + +check_for_service_routing [msg] { + msg := expect.response_service_is(Response, "echo-slash-blue") +} + +--- + +apiVersion: networking.x-k8s.io/v1alpha1 +kind: TLSRoute +metadata: + name: tlsroute1 +spec: + rules: + - matches: + forwardTo: + - serviceName: echo-slash-blue + port: 443 + +--- + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + + # Ensure / request returns 200 status code. +Response := client.Get({ +"url": url.https("/"), + "headers": { + "Host": "anything.should.work.now", + "User-Agent": client.ua("tlsroute"), + }, +"tls_insecure_skip_verify": true, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 200) +} + +check_for_service_routing [msg] { + msg := expect.response_service_is(Response, "echo-slash-blue") +} \ No newline at end of file diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index 5b2f3239d03..cd499c91e7d 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -2266,6 +2266,256 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, ), }, + "basic TLSRoute": { + gateway: gatewayWithSelector, + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + Labels: map[string]string{ + "app": "contour", + "type": "controller", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{ + "tcp.projectcontour.io", + }, + }}, + ForwardTo: tcpRouteForwardTo("kuard", 8080, 0), + }}, + }, + }, + }, + want: listeners( + &Listener{ + Port: 443, + VirtualHosts: virtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "tcp.projectcontour.io", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + ), + }, + ), + }, + "TLSRoute with mutiple SNIs": { + gateway: gatewayWithSelector, + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + Labels: map[string]string{ + "app": "contour", + "type": "controller", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{ + "tcp.projectcontour.io", + "another.projectcontour.io", + "thing.projectcontour.io", + }, + }}, + ForwardTo: tcpRouteForwardTo("kuard", 8080, 0), + }}, + }, + }, + }, + want: listeners( + &Listener{ + Port: 443, + VirtualHosts: virtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "another.projectcontour.io", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "tcp.projectcontour.io", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "thing.projectcontour.io", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + ), + }, + ), + }, + "TLSRoute with mutiple SNIs, one is invalid": { + gateway: gatewayWithSelector, + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + Labels: map[string]string{ + "app": "contour", + "type": "controller", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{ + "tcp.projectcontour.io", + "*.*.another.projectcontour.io", + "thing.projectcontour.io", + }, + }}, + ForwardTo: tcpRouteForwardTo("kuard", 8080, 0), + }}, + }, + }, + }, + want: listeners( + &Listener{ + Port: 443, + VirtualHosts: virtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "tcp.projectcontour.io", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "thing.projectcontour.io", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + ), + }, + ), + }, + "TLSRoute with mutiple SNIs, all are invalid": { + gateway: gatewayWithSelector, + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + Labels: map[string]string{ + "app": "contour", + "type": "controller", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{ + "tcp.*.projectcontour.io", + "*.*.another.projectcontour.io", + "!!thing.projectcontour.io", + }, + }}, + ForwardTo: tcpRouteForwardTo("kuard", 8080, 0), + }}, + }, + }, + }, + want: listeners(), + }, + "TLSRoute without any hostnames specified results in '*' match all": { + gateway: gatewayWithSelector, + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + Labels: map[string]string{ + "app": "contour", + "type": "controller", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{}}, + ForwardTo: tcpRouteForwardTo("kuard", 8080, 0), + }}, + }, + }, + }, + want: listeners( + &Listener{ + Port: 443, + VirtualHosts: virtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + ListenerName: "ingress_https", + }, + TCPProxy: &TCPProxy{ + Clusters: clusters(service(kuardService)), + }, + }, + ), + }, + ), + }, + "TLSRoute with missing forwardTo service": { + gateway: gatewayWithSelector, + objs: []interface{}{ + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + Labels: map[string]string{ + "app": "contour", + "type": "controller", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{ + "tcp.projectcontour.io", + }, + }}, + ForwardTo: tcpRouteForwardTo("kuard", 8080, 0), + }}, + }, + }, + }, + want: listeners(), + }, } for name, tc := range tests { @@ -10915,6 +11165,14 @@ func httpRouteForwardTo(serviceName string, port int, weight int32) []gatewayapi }} } +func tcpRouteForwardTo(serviceName string, port int, weight int32) []gatewayapi_v1alpha1.RouteForwardTo { + return []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr(serviceName), + Port: gatewayPort(port), + Weight: pointer.Int32Ptr(weight), + }} +} + func prefixroute(prefix string, first *Service, rest ...*Service) *Route { services := append([]*Service{first}, rest...) return &Route{ diff --git a/internal/dag/cache.go b/internal/dag/cache.go index 10b61a6cd3d..66546076cb8 100644 --- a/internal/dag/cache.go +++ b/internal/dag/cache.go @@ -248,10 +248,6 @@ func (kc *KubernetesCache) Insert(obj interface{}) bool { kc.udproutes[k8s.NamespacedNameOf(obj)] = obj return true case *gatewayapi_v1alpha1.TLSRoute: - m := k8s.NamespacedNameOf(obj) - // TODO(youngnick): Remove this once gateway-api actually have behavior - // other than being added to the cache. - kc.WithField("experimental", "gateway-api").WithField("name", m.Name).WithField("namespace", m.Namespace).Debug("Adding TLSRoute") kc.tlsroutes[k8s.NamespacedNameOf(obj)] = obj return true case *gatewayapi_v1alpha1.BackendPolicy: @@ -450,9 +446,6 @@ func (kc *KubernetesCache) remove(obj interface{}) bool { case *gatewayapi_v1alpha1.TLSRoute: m := k8s.NamespacedNameOf(obj) _, ok := kc.tlsroutes[m] - // TODO(youngnick): Remove this once gateway-api actually have behavior - // other than being removed from the cache. - kc.WithField("experimental", "gateway-api").WithField("name", m.Name).WithField("namespace", m.Namespace).Debug("Removing TLSRoute") delete(kc.tlsroutes, m) return ok case *gatewayapi_v1alpha1.BackendPolicy: diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index 1933d8dfcc8..e8a8ce90018 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -19,6 +19,8 @@ import ( "net/http" "strings" + "github.com/projectcontour/contour/internal/k8s" + "k8s.io/utils/pointer" "github.com/projectcontour/contour/internal/status" @@ -33,6 +35,7 @@ import ( const ( KindHTTPRoute = "HTTPRoute" + KindTLSRoute = "TLSRoute" ) // GatewayAPIProcessor translates Gateway API types into DAG @@ -70,12 +73,13 @@ func (p *GatewayAPIProcessor) Run(dag *DAG, source *KubernetesCache) { for _, listener := range p.source.gateway.Spec.Listeners { - var matchingRoutes []*gatewayapi_v1alpha1.HTTPRoute + var matchingHTTPRoutes []*gatewayapi_v1alpha1.HTTPRoute + var matchingTLSRoutes []*gatewayapi_v1alpha1.TLSRoute var listenerSecret *Secret // Validate the Kind on the selector is a supported type. switch listener.Protocol { - case gatewayapi_v1alpha1.HTTPSProtocolType, gatewayapi_v1alpha1.TLSProtocolType: + case gatewayapi_v1alpha1.HTTPSProtocolType: // Validate that if protocol is type HTTPS or TLS that TLS is defined. if listener.TLS == nil { p.Errorf("Listener.TLS is required when protocol is %q.", listener.Protocol) @@ -88,7 +92,7 @@ func (p *GatewayAPIProcessor) Run(dag *DAG, source *KubernetesCache) { // routes to be bound to this listener since it can't serve TLS traffic. continue } - case gatewayapi_v1alpha1.HTTPProtocolType: + case gatewayapi_v1alpha1.HTTPProtocolType, gatewayapi_v1alpha1.TLSProtocolType: break default: p.Errorf("Listener.Protocol %q is not supported.", listener.Protocol) @@ -104,7 +108,7 @@ func (p *GatewayAPIProcessor) Run(dag *DAG, source *KubernetesCache) { } // Validate the Kind on the selector is a supported type. - if listener.Routes.Kind != KindHTTPRoute { + if listener.Routes.Kind != KindHTTPRoute && listener.Routes.Kind != KindTLSRoute { p.Errorf("Listener.Routes.Kind %q is not supported.", listener.Routes.Kind) continue } @@ -121,7 +125,7 @@ func (p *GatewayAPIProcessor) Run(dag *DAG, source *KubernetesCache) { // with the Gateway. If this Selector is defined, only routes matching the Selector // are associated with the Gateway. An empty Selector matches all routes. - nsMatches, err := p.namespaceMatches(listener.Routes.Namespaces, route) + nsMatches, err := p.namespaceMatches(listener.Routes.Namespaces, route.Namespace) if err != nil { p.Errorf("error validating namespaces against Listener.Routes.Namespaces: %s", err) } @@ -140,7 +144,7 @@ func (p *GatewayAPIProcessor) Run(dag *DAG, source *KubernetesCache) { // If a label selector or namespace selector matches, but the gateway Allow doesn't // then set the "Admitted: false" for the route. - routeAccessor, commit := p.dag.StatusCache.HTTPRouteAccessor(route) + routeAccessor, commit := p.dag.StatusCache.ConditionsAccessor(k8s.NamespacedNameOf(route), route.Generation, status.ResourceHTTPRoute, route.Status.Gateways) routeAccessor.AddCondition(gatewayapi_v1alpha1.ConditionRouteAdmitted, metav1.ConditionFalse, status.ReasonGatewayAllowMismatch, "Gateway RouteSelector matches, but GatewayAllow has mismatch.") commit() continue @@ -148,15 +152,48 @@ func (p *GatewayAPIProcessor) Run(dag *DAG, source *KubernetesCache) { if gatewayAllowMatches { // Empty Selector matches all routes. - matchingRoutes = append(matchingRoutes, route) + matchingHTTPRoutes = append(matchingHTTPRoutes, route) } } } - // Process all the routes that match this Gateway. - for _, matchingRoute := range matchingRoutes { + for _, route := range p.source.tlsroutes { + // Filter the TLSRoutes that match the gateway which Contour is configured to watch. + // RouteBindingSelector defines a schema for associating routes with the Gateway. + // If Namespaces and Selector are defined, only routes matching both selectors are associated with the Gateway. + + // ## RouteBindingSelector ## + // + // Selector specifies a set of route labels used for selecting routes to associate + // with the Gateway. If this Selector is defined, only routes matching the Selector + // are associated with the Gateway. An empty Selector matches all routes. + + nsMatches, err := p.namespaceMatches(listener.Routes.Namespaces, route.Namespace) + if err != nil { + p.Errorf("error validating namespaces against Listener.Routes.Namespaces: %s", err) + } + + selMatches, err := selectorMatches(listener.Routes.Selector, route.Labels) + if err != nil { + p.Errorf("error validating routes against Listener.Routes.Selector: %s", err) + } + + if selMatches && nsMatches { + // Empty Selector matches all routes. + matchingTLSRoutes = append(matchingTLSRoutes, route) + } + } + + // Process all the HTTPRoutes that match this Gateway. + for _, matchingRoute := range matchingHTTPRoutes { p.computeHTTPRoute(matchingRoute, listenerSecret) } + + // Process all the routes that match this Gateway. + for _, matchingRoute := range matchingTLSRoutes { + fmt.Println("----processing TLS Route: ", matchingRoute.Name) + p.computeTLSRoute(matchingRoute) + } } } @@ -193,42 +230,47 @@ func isSecretRef(certificateRef *gatewayapi_v1alpha1.LocalObjectReference) bool return strings.ToLower(certificateRef.Kind) == "secret" && strings.ToLower(certificateRef.Group) == "core" } -func (p *GatewayAPIProcessor) computeHosts(route *gatewayapi_v1alpha1.HTTPRoute) ([]string, []error) { +func (p *GatewayAPIProcessor) computeHosts(hostnames []gatewayapi_v1alpha1.Hostname) ([]string, []error) { // Determine the hosts on the route, if no hosts // are defined, then set to "*". var hosts []string var errors []error - if len(route.Spec.Hostnames) == 0 { + if len(hostnames) == 0 { hosts = append(hosts, "*") return hosts, nil } - for _, host := range route.Spec.Hostnames { + for _, host := range hostnames { hostname := string(host) - if isIP := net.ParseIP(hostname) != nil; isIP { - errors = append(errors, fmt.Errorf("hostname %q must be a DNS name, not an IP address", hostname)) + if err := validHostName(hostname); err != nil { + errors = append(errors, err) continue } - if strings.Contains(hostname, "*") { - if errs := validation.IsWildcardDNS1123Subdomain(hostname); errs != nil { - errors = append(errors, fmt.Errorf("invalid hostname %q: %v", hostname, errs)) - continue - } - } else { - if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { - errors = append(errors, fmt.Errorf("invalid listener hostname %q: %v", hostname, errs)) - continue - } - } hosts = append(hosts, string(host)) } return hosts, errors } +func validHostName(hostname string) error { + if isIP := net.ParseIP(hostname) != nil; isIP { + return fmt.Errorf("hostname %q must be a DNS name, not an IP address", hostname) + } + if strings.Contains(hostname, "*") { + if errs := validation.IsWildcardDNS1123Subdomain(hostname); errs != nil { + return fmt.Errorf("invalid hostname %q: %v", hostname, errs) + } + } else { + if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { + return fmt.Errorf("invalid listener hostname %q: %v", hostname, errs) + } + } + return nil +} + // namespaceMatches returns true if the namespaces selector matches // the HTTPRoute that is being processed. -func (p *GatewayAPIProcessor) namespaceMatches(namespaces *gatewayapi_v1alpha1.RouteNamespaces, route *gatewayapi_v1alpha1.HTTPRoute) (bool, error) { +func (p *GatewayAPIProcessor) namespaceMatches(namespaces *gatewayapi_v1alpha1.RouteNamespaces, namespace string) (bool, error) { // From indicates where Routes will be selected for this Gateway. // Possible values are: // * All: Routes in all namespaces may be used by this Gateway. @@ -248,14 +290,14 @@ func (p *GatewayAPIProcessor) namespaceMatches(namespaces *gatewayapi_v1alpha1.R case gatewayapi_v1alpha1.RouteSelectAll: return true, nil case gatewayapi_v1alpha1.RouteSelectSame: - return p.source.ConfiguredGateway.Namespace == route.Namespace, nil + return p.source.ConfiguredGateway.Namespace == namespace, nil case gatewayapi_v1alpha1.RouteSelectSelector: if len(namespaces.Selector.MatchLabels) == 0 || len(namespaces.Selector.MatchExpressions) == 0 { return false, fmt.Errorf("RouteNamespaces selector must be specified when `RouteSelectType=Selector`") } // Look up the HTTPRoute's namespace in the list of cached namespaces. - if ns := p.source.namespaces[route.Namespace]; ns != nil { + if ns := p.source.namespaces[namespace]; ns != nil { // Check that the route's namespace is included in the Gateway's // namespace selector/expression. @@ -313,11 +355,106 @@ func selectorMatches(selector *metav1.LabelSelector, objLabels map[string]string return true, nil } +func (p *GatewayAPIProcessor) computeTLSRoute(route *gatewayapi_v1alpha1.TLSRoute) { + + routeAccessor, commit := p.dag.StatusCache.ConditionsAccessor(k8s.NamespacedNameOf(route), route.Generation, status.ResourceTLSRoute, route.Status.Gateways) + defer commit() + + for _, rule := range route.Spec.Rules { + var hosts []string + var matchErrors []error + totalSnis := 0 + + // Build the set of SNIs that are applied to this TLSRoute. + for _, match := range rule.Matches { + for _, snis := range match.SNIs { + totalSnis++ + if err := validHostName(string(snis)); err != nil { + matchErrors = append(matchErrors, err) + continue + } + hosts = append(hosts, string(snis)) + } + } + + // If there are any errors with the supplied hostnames, then + // add a condition to the route. + for _, err := range matchErrors { + routeAccessor.AddCondition(status.ConditionResolvedRefs, metav1.ConditionFalse, status.ReasonDegraded, err.Error()) + } + + // If all the supplied SNIs are invalid, then this route is invalid + // and should be dropped. + if len(matchErrors) != 0 && len(matchErrors) == totalSnis { + continue + } + + // If SNIs is unspecified, then all + // requests associated with the gateway TLS listener will match. + // This can be used to define a default backend for a TLS listener. + if len(hosts) == 0 { + hosts = []string{"*"} + } + + if len(rule.ForwardTo) == 0 { + routeAccessor.AddCondition(status.ConditionResolvedRefs, metav1.ConditionFalse, status.ReasonDegraded, "At least one Spec.Rules.ForwardTo must be specified.") + continue + } + + var proxy TCPProxy + for _, forward := range rule.ForwardTo { + // Verify the service is valid + if forward.ServiceName == nil { + routeAccessor.AddCondition(status.ConditionResolvedRefs, metav1.ConditionFalse, status.ReasonDegraded, "Spec.Rules.ForwardTo.ServiceName must be specified.") + continue + } + + // TODO: Do not require port to be present (#3352). + if forward.Port == nil { + routeAccessor.AddCondition(status.ConditionResolvedRefs, metav1.ConditionFalse, status.ReasonDegraded, "Spec.Rules.ForwardTo.ServicePort must be specified.") + continue + } + + meta := types.NamespacedName{Name: *forward.ServiceName, Namespace: route.Namespace} + + // TODO: Refactor EnsureService to take an int32 so conversion to intstr is not needed. + service, err := p.dag.EnsureService(meta, intstr.FromInt(int(*forward.Port)), p.source) + if err != nil { + routeAccessor.AddCondition(status.ConditionResolvedRefs, metav1.ConditionFalse, status.ReasonDegraded, fmt.Sprintf("Service %q does not exist", meta.Name)) + continue + } + proxy.Clusters = append(proxy.Clusters, &Cluster{ + Upstream: service, + SNI: service.ExternalName, + }) + } + + if len(proxy.Clusters) == 0 { + // No valid clusters so the route should get rejected. + continue + } + + for _, host := range hosts { + secure := p.dag.EnsureSecureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_https"}) + secure.TCPProxy = &proxy + } + } + + // Determine if any errors exist in conditions and set the "Admitted" + // condition accordingly. + switch len(routeAccessor.Conditions) { + case 0: + routeAccessor.AddCondition(gatewayapi_v1alpha1.ConditionRouteAdmitted, metav1.ConditionTrue, status.ReasonValid, "Valid TLSRoute") + default: + routeAccessor.AddCondition(gatewayapi_v1alpha1.ConditionRouteAdmitted, metav1.ConditionFalse, status.ReasonErrorsExist, "Errors found, check other Conditions for details.") + } +} + func (p *GatewayAPIProcessor) computeHTTPRoute(route *gatewayapi_v1alpha1.HTTPRoute, listenerSecret *Secret) { - routeAccessor, commit := p.dag.StatusCache.HTTPRouteAccessor(route) + routeAccessor, commit := p.dag.StatusCache.ConditionsAccessor(k8s.NamespacedNameOf(route), route.Generation, status.ResourceHTTPRoute, route.Status.Gateways) defer commit() - hosts, errs := p.computeHosts(route) + hosts, errs := p.computeHosts(route.Spec.Hostnames) for _, err := range errs { routeAccessor.AddCondition(status.ConditionResolvedRefs, metav1.ConditionFalse, status.ReasonDegraded, err.Error()) } diff --git a/internal/dag/gatewayapi_processor_test.go b/internal/dag/gatewayapi_processor_test.go index 060d4dfc635..d49a8ffe605 100644 --- a/internal/dag/gatewayapi_processor_test.go +++ b/internal/dag/gatewayapi_processor_test.go @@ -196,7 +196,7 @@ func TestComputeHosts(t *testing.T) { FieldLogger: fixture.NewTestLogger(t), } - got, gotError := processor.computeHosts(tc.route) + got, gotError := processor.computeHosts(tc.route.Spec.Hostnames) assert.Equal(t, tc.want, got) assert.Equal(t, tc.wantError, gotError) }) diff --git a/internal/dag/status_test.go b/internal/dag/status_test.go index c21f3a4d3e4..d7d6614088f 100644 --- a/internal/dag/status_test.go +++ b/internal/dag/status_test.go @@ -2538,7 +2538,7 @@ func TestGatewayAPIDAGStatus(t *testing.T) { builder.Source.Insert(o) } dag := builder.Build() - updates := dag.StatusCache.GetHTTPRouteUpdates() + updates := dag.StatusCache.GetXRouteUpdates() var gotConditions []metav1.Condition for _, u := range updates { @@ -3280,4 +3280,280 @@ func TestGatewayAPIDAGStatus(t *testing.T) { Message: "Gateway RouteSelector matches, but GatewayAllow has mismatch.", }}, }) + + run(t, "TLSRoute: spec.rules.forwardTo.serviceName not specified", testcase{ + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"test.projectcontour.io"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: nil, + Port: gatewayPort(8080), + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + Message: "Spec.Rules.ForwardTo.ServiceName must be specified.", + }, { + Type: "Admitted", + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) + + run(t, "TLSRoute: spec.rules.forwardTo.serviceName invalid on two matches", testcase{ + objs: []interface{}{ + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"test.projectcontour.io"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("invalid-one"), + Port: gatewayPort(8080), + }}, + }, { + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"another.projectcontour.io"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("invalid-two"), + Port: gatewayPort(8080), + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + Message: "Service \"invalid-one\" does not exist, Service \"invalid-two\" does not exist", + }, { + Type: "Admitted", + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) + + run(t, "TLSRoute: spec.rules.forwardTo.servicePort not specified", testcase{ + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"test.projectcontour.io"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("kuard"), + Port: nil, + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + Message: "Spec.Rules.ForwardTo.ServicePort must be specified.", + }, { + Type: string(gatewayapi_v1alpha1.ConditionRouteAdmitted), + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) + + run(t, "TLSRoute: spec.rules.forwardTo not specified", testcase{ + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"test.projectcontour.io"}, + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + Message: "At least one Spec.Rules.ForwardTo must be specified.", + }, { + Type: "Admitted", + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) + + run(t, "TLSRoute: spec.rules.hostname: invalid wildcard", testcase{ + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"*.*.projectcontour.io"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("kuard"), + Port: gatewayPort(8080), + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + 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])?)*')]", + }, { + Type: "Admitted", + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) + + run(t, "TLSRoute: spec.rules.hostname: invalid hostname", testcase{ + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"#projectcontour.io"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("kuard"), + Port: gatewayPort(8080), + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + Message: "invalid listener hostname \"#projectcontour.io\": [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])?)*')]", + }, { + Type: "Admitted", + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) + + run(t, "TLSRoute: spec.rules.hostname: invalid hostname, ip address", testcase{ + objs: []interface{}{ + kuardService, + &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + Labels: map[string]string{ + "app": "contour", + }, + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Gateways: &gatewayapi_v1alpha1.RouteGateways{ + Allow: gatewayAllowTypePtr(gatewayapi_v1alpha1.GatewayAllowAll), + }, + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{"1.2.3.4"}, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("kuard"), + Port: gatewayPort(8080), + }}, + }}, + }, + }}, + want: []metav1.Condition{{ + Type: string(status.ConditionResolvedRefs), + Status: contour_api_v1.ConditionFalse, + Reason: string(status.ReasonDegraded), + Message: "hostname \"1.2.3.4\" must be a DNS name, not an IP address", + }, { + Type: "Admitted", + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorsExist", + Message: "Errors found, check other Conditions for details.", + }}, + }) } diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index 899211333b7..42ff3b78eb5 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -647,10 +647,21 @@ func FilterExternalAuthz(authzClusterName string, failOpen bool, timeout timeout func FilterChainTLS(domain string, downstream *envoy_tls_v3.DownstreamTlsContext, filters []*envoy_listener_v3.Filter) *envoy_listener_v3.FilterChain { fc := &envoy_listener_v3.FilterChain{ Filters: filters, - FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + } + + // If the domain doesn't have a specific SNI, Envoy can't filter + // on that, so change the Match to be on TransportProtocol which would + // match any request over TLS to this listener. + if domain == "*" { + fc.FilterChainMatch = &envoy_listener_v3.FilterChainMatch{ + TransportProtocol: "tls", + } + } else { + fc.FilterChainMatch = &envoy_listener_v3.FilterChainMatch{ ServerNames: []string{domain}, - }, + } } + // Attach TLS data to this listener if provided. if downstream != nil { fc.TransportSocket = DownstreamTLSTransportSocket(downstream) diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index d1272517e5f..aadb21fc48e 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -53,6 +53,7 @@ func TestProtoNamesForVersions(t *testing.T) { assert.Equal(t, ProtoNamesForVersions(HTTPVersion3), []string(nil)) assert.Equal(t, ProtoNamesForVersions(HTTPVersion1, HTTPVersion2), []string{"h2", "http/1.1"}) } + func TestListener(t *testing.T) { tests := map[string]struct { name, address string @@ -1368,6 +1369,40 @@ func TestTCPProxy(t *testing.T) { } } +func TestFilterChainTLS_Match(t *testing.T) { + + tests := map[string]struct { + domain string + downstream *envoy_tls_v3.DownstreamTlsContext + filters []*envoy_listener_v3.Filter + want *envoy_listener_v3.FilterChain + }{ + "SNI": { + domain: "projectcontour.io", + want: &envoy_listener_v3.FilterChain{ + FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + ServerNames: []string{"projectcontour.io"}, + }, + }, + }, + "No SNI": { + domain: "*", + want: &envoy_listener_v3.FilterChain{ + FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + TransportProtocol: "tls", + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := FilterChainTLS(tc.domain, tc.downstream, tc.filters) + protobuf.ExpectEqual(t, tc.want, got) + }) + } +} + // TestBuilderValidation tests that validation checks that // DefaultFilters adds the required HTTP connection manager filters. func TestBuilderValidation(t *testing.T) { diff --git a/internal/featuretests/v3/tlsroute_test.go b/internal/featuretests/v3/tlsroute_test.go new file mode 100644 index 00000000000..dfe786eeea1 --- /dev/null +++ b/internal/featuretests/v3/tlsroute_test.go @@ -0,0 +1,169 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v3 + +import ( + 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" + "github.com/projectcontour/contour/internal/dag" + envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" + "github.com/projectcontour/contour/internal/fixture" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + gatewayapi_v1alpha1 "sigs.k8s.io/gateway-api/apis/v1alpha1" + "testing" +) + +func TestTLSRoute(t *testing.T) { + rh, c, done := setup(t) + defer done() + + //s1 := &v1.Secret{ + // ObjectMeta: metav1.ObjectMeta{ + // Name: "secret", + // Namespace: "default", + // }, + // Type: "kubernetes.io/tls", + // Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + //} + + svc := fixture.NewService("correct-backend"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}) + + //rh.OnAdd(s1) + rh.OnAdd(svc) + + rh.OnAdd(&gatewayapi_v1alpha1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "contour", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1alpha1.GatewaySpec{ + Listeners: []gatewayapi_v1alpha1.Listener{{ + Port: 443, + Protocol: "TLS", + Routes: gatewayapi_v1alpha1.RouteBindingSelector{ + Namespaces: &gatewayapi_v1alpha1.RouteNamespaces{ + From: routeSelectTypePtr(gatewayapi_v1alpha1.RouteSelectAll), + }, + Kind: dag.KindTLSRoute, + }, + }}, + }, + }) + + route1 := &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + Matches: []gatewayapi_v1alpha1.TLSRouteMatch{{ + SNIs: []gatewayapi_v1alpha1.Hostname{ + "tcp.projectcontour.io", + }, + }}, + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("correct-backend"), + Port: gatewayPort(80), + }}, + }}, + }, + } + + rh.OnAdd(route1) + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + &envoy_listener_v3.Listener{ + Name: "ingress_https", + 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"), + ), + FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + ServerNames: []string{"tcp.projectcontour.io"}, + }, + }}, + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }, + staticListener(), + ), + 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, + }) + + // Route2 doesn't define any SNIs, so this should become the default backend. + route2 := &gatewayapi_v1alpha1.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha1.TLSRouteSpec{ + Rules: []gatewayapi_v1alpha1.TLSRouteRule{{ + ForwardTo: []gatewayapi_v1alpha1.RouteForwardTo{{ + ServiceName: pointer.StringPtr("correct-backend"), + Port: gatewayPort(80), + }}, + }}, + }, + } + + rh.OnUpdate(route1, route2) + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + &envoy_listener_v3.Listener{ + Name: "ingress_https", + 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"), + ), + FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + TransportProtocol: "tls", + }, + }}, + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }, + staticListener(), + ), + 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, + }) +} diff --git a/internal/status/cache.go b/internal/status/cache.go index c12d336fe32..2f0d1903305 100644 --- a/internal/status/cache.go +++ b/internal/status/cache.go @@ -35,10 +35,10 @@ const ValidCondition ConditionType = "Valid" // NewCache creates a new Cache for holding status updates. func NewCache(gateway types.NamespacedName) Cache { return Cache{ - proxyUpdates: make(map[types.NamespacedName]*ProxyUpdate), - gatewayRef: gateway, - httpRouteUpdates: make(map[types.NamespacedName]*HTTPRouteUpdate), - entries: make(map[string]map[types.NamespacedName]CacheEntry), + proxyUpdates: make(map[types.NamespacedName]*ProxyUpdate), + gatewayRef: gateway, + xRouteUpdates: make(map[types.NamespacedName]*ConditionsUpdate), + entries: make(map[string]map[types.NamespacedName]CacheEntry), } } @@ -53,8 +53,8 @@ type CacheEntry interface { type Cache struct { proxyUpdates map[types.NamespacedName]*ProxyUpdate - gatewayRef types.NamespacedName - httpRouteUpdates map[types.NamespacedName]*HTTPRouteUpdate + gatewayRef types.NamespacedName + xRouteUpdates map[types.NamespacedName]*ConditionsUpdate // Map of cache entry maps, keyed on Kind. entries map[string]map[types.NamespacedName]CacheEntry @@ -100,13 +100,13 @@ func (c *Cache) GetStatusUpdates() []k8s.StatusUpdate { flattened = append(flattened, update) } - for fullname, routeUpdate := range c.httpRouteUpdates { + for fullname, routeUpdate := range c.xRouteUpdates { update := k8s.StatusUpdate{ NamespacedName: fullname, Resource: schema.GroupVersionResource{ Group: gatewayapi_v1alpha1.GroupVersion.Group, Version: gatewayapi_v1alpha1.GroupVersion.Version, - Resource: "httproutes", + Resource: routeUpdate.Resource, }, Mutator: routeUpdate, } @@ -135,12 +135,11 @@ func (c *Cache) GetProxyUpdates() []*ProxyUpdate { return allUpdates } -// GetHTTPRouteUpdates gets the underlying HTTPRouteUpdate objects -// from the cache. -func (c *Cache) GetHTTPRouteUpdates() []*HTTPRouteUpdate { - var allUpdates []*HTTPRouteUpdate - for _, httpRouteUpdate := range c.httpRouteUpdates { - allUpdates = append(allUpdates, httpRouteUpdate) +// GetXRouteUpdates gets the underlying ConditionsUpdate objects from the cache. +func (c *Cache) GetXRouteUpdates() []*ConditionsUpdate { + var allUpdates []*ConditionsUpdate + for _, conditionsUpdate := range c.xRouteUpdates { + allUpdates = append(allUpdates, conditionsUpdate) } return allUpdates } diff --git a/internal/status/httproutestatus.go b/internal/status/conditions.go similarity index 70% rename from internal/status/httproutestatus.go rename to internal/status/conditions.go index 8926c72b795..8b57001a322 100644 --- a/internal/status/httproutestatus.go +++ b/internal/status/conditions.go @@ -17,12 +17,14 @@ import ( "fmt" "time" - "github.com/projectcontour/contour/internal/k8s" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" gatewayapi_v1alpha1 "sigs.k8s.io/gateway-api/apis/v1alpha1" ) +const ResourceHTTPRoute = "httproutes" +const ResourceTLSRoute = "tlsroutes" + const ConditionNotImplemented gatewayapi_v1alpha1.RouteConditionType = "NotImplemented" const ConditionResolvedRefs gatewayapi_v1alpha1.RouteConditionType = "ResolvedRefs" @@ -37,17 +39,18 @@ const ReasonValid RouteReasonType = "Valid" const ReasonErrorsExist RouteReasonType = "ErrorsExist" const ReasonGatewayAllowMismatch RouteReasonType = "GatewayAllowMismatch" -type HTTPRouteUpdate struct { +type ConditionsUpdate struct { FullName types.NamespacedName Conditions map[gatewayapi_v1alpha1.RouteConditionType]metav1.Condition ExistingConditions map[gatewayapi_v1alpha1.RouteConditionType]metav1.Condition GatewayRef types.NamespacedName + Resource string Generation int64 TransitionTime metav1.Time } // AddCondition returns a metav1.Condition for a given ConditionType. -func (routeUpdate *HTTPRouteUpdate) AddCondition(cond gatewayapi_v1alpha1.RouteConditionType, status metav1.ConditionStatus, reason RouteReasonType, message string) metav1.Condition { +func (routeUpdate *ConditionsUpdate) AddCondition(cond gatewayapi_v1alpha1.RouteConditionType, status metav1.ConditionStatus, reason RouteReasonType, message string) metav1.Condition { if c, ok := routeUpdate.Conditions[cond]; ok { message = fmt.Sprintf("%s, %s", c.Message, message) @@ -65,41 +68,34 @@ func (routeUpdate *HTTPRouteUpdate) AddCondition(cond gatewayapi_v1alpha1.RouteC return newDc } -// HTTPRouteAccessor returns a HTTPRouteUpdate that allows a client to build up a list of +// ConditionsAccessor returns a ConditionsUpdate that allows a client to build up a list of // metav1.Conditions as well as a function to commit the change back to the cache when everything -// is done. The commit function pattern is used so that the HTTPRouteUpdate does not need +// is done. The commit function pattern is used so that the ConditionsUpdate does not need // to know anything the cache internals. -func (c *Cache) HTTPRouteAccessor(route *gatewayapi_v1alpha1.HTTPRoute) (*HTTPRouteUpdate, func()) { - pu := &HTTPRouteUpdate{ - FullName: k8s.NamespacedNameOf(route), +func (c *Cache) ConditionsAccessor(nsName types.NamespacedName, generation int64, resource string, gateways []gatewayapi_v1alpha1.RouteGatewayStatus) (*ConditionsUpdate, func()) { + pu := &ConditionsUpdate{ + FullName: nsName, Conditions: make(map[gatewayapi_v1alpha1.RouteConditionType]metav1.Condition), - ExistingConditions: c.getGatewayConditions(route.Status.Gateways), + ExistingConditions: c.getGatewayConditions(gateways), GatewayRef: c.gatewayRef, - Generation: route.Generation, + Generation: generation, TransitionTime: metav1.NewTime(time.Now()), + Resource: resource, } return pu, func() { - c.commitHTTPRoute(pu) + c.commitRoute(pu) } } -func (c *Cache) commitHTTPRoute(pu *HTTPRouteUpdate) { +func (c *Cache) commitRoute(pu *ConditionsUpdate) { if len(pu.Conditions) == 0 { return } - c.httpRouteUpdates[pu.FullName] = pu + c.xRouteUpdates[pu.FullName] = pu } -func (routeUpdate *HTTPRouteUpdate) Mutate(obj interface{}) interface{} { - o, ok := obj.(*gatewayapi_v1alpha1.HTTPRoute) - if !ok { - panic(fmt.Sprintf("Unsupported %T object %s/%s in HTTPRouteUpdate status mutator", - obj, routeUpdate.FullName.Namespace, routeUpdate.FullName.Name, - )) - } - - httpRoute := o.DeepCopy() +func (routeUpdate *ConditionsUpdate) Mutate(obj interface{}) interface{} { var gatewayStatuses []gatewayapi_v1alpha1.RouteGatewayStatus var conditionsToWrite []metav1.Condition @@ -107,11 +103,11 @@ func (routeUpdate *HTTPRouteUpdate) Mutate(obj interface{}) interface{} { for _, cond := range routeUpdate.Conditions { // set the Condition's observed generation based on - // the generation of the HTTPRoute we looked at. + // the generation of the xRoute we looked at. cond.ObservedGeneration = routeUpdate.Generation cond.LastTransitionTime = routeUpdate.TransitionTime - // is there a newer Condition on the HTTPRoute matching + // is there a newer Condition on the xRoute matching // this condition's type? If so, our observation is stale, // so don't write it, keep the newer one instead. var newerConditionExists bool @@ -128,7 +124,7 @@ func (routeUpdate *HTTPRouteUpdate) Mutate(obj interface{}) interface{} { } // if we didn't find a newer version of the Condition on the - // HTTPRoute, then write the one we computed. + // xRoute, then write the one we computed. if !newerConditionExists { conditionsToWrite = append(conditionsToWrite, cond) } @@ -142,20 +138,40 @@ func (routeUpdate *HTTPRouteUpdate) Mutate(obj interface{}) interface{} { Conditions: conditionsToWrite, }) + switch o := obj.(type) { + case *gatewayapi_v1alpha1.HTTPRoute: + xRoute := o.DeepCopy() + + // Set the GatewayStatuses. + xRoute.Status.RouteStatus.Gateways = append(gatewayStatuses, routeUpdate.combineConditions(xRoute.Status.Gateways)...) + return xRoute + case *gatewayapi_v1alpha1.TLSRoute: + xRoute := o.DeepCopy() + + // Set the GatewayStatuses. + xRoute.Status.RouteStatus.Gateways = append(gatewayStatuses, routeUpdate.combineConditions(xRoute.Status.Gateways)...) + return xRoute + default: + panic(fmt.Sprintf("Unsupported %T object %s/%s in ConditionsUpdate status mutator", + obj, routeUpdate.FullName.Namespace, routeUpdate.FullName.Name, + )) + } +} + +func (routeUpdate *ConditionsUpdate) combineConditions(gwStatus []gatewayapi_v1alpha1.RouteGatewayStatus) []gatewayapi_v1alpha1.RouteGatewayStatus { + + var gatewayStatuses []gatewayapi_v1alpha1.RouteGatewayStatus + // Now that we have all the conditions, add them back to the object // to get written out. - for _, rgs := range httpRoute.Status.Gateways { + for _, rgs := range gwStatus { if rgs.GatewayRef.Name == routeUpdate.GatewayRef.Name && rgs.GatewayRef.Namespace == routeUpdate.GatewayRef.Namespace { continue } else { gatewayStatuses = append(gatewayStatuses, rgs) } } - - // Set the GatewayStatuses. - httpRoute.Status.RouteStatus.Gateways = gatewayStatuses - - return httpRoute + return gatewayStatuses } func (c *Cache) getGatewayConditions(gatewayStatus []gatewayapi_v1alpha1.RouteGatewayStatus) map[gatewayapi_v1alpha1.RouteConditionType]metav1.Condition { diff --git a/internal/status/httproutestatus_test.go b/internal/status/conditions_test.go similarity index 98% rename from internal/status/httproutestatus_test.go rename to internal/status/conditions_test.go index b1953821bff..472ab1a3541 100644 --- a/internal/status/httproutestatus_test.go +++ b/internal/status/conditions_test.go @@ -35,7 +35,7 @@ func TestHTTPRouteAddCondition(t *testing.T) { ObservedGeneration: testGeneration, } - httpRouteUpdate := HTTPRouteUpdate{ + httpRouteUpdate := ConditionsUpdate{ FullName: k8s.NamespacedNameFrom("test/test"), Generation: testGeneration, Conditions: make(map[gatewayapi_v1alpha1.RouteConditionType]metav1.Condition),