From 5c65e8a10849af504ca15d6d70ec3f0a981e7eec Mon Sep 17 00:00:00 2001 From: Steve Kriss Date: Tue, 2 Nov 2021 08:37:09 -0600 Subject: [PATCH] drop Visitor pattern for getting info out of DAG (#4099) Replaces the use of the visitor pattern for getting data out of the DAG with "accessor" functions that know about the structure of the DAG and can fetch information efficiently. Closes #4072. Signed-off-by: Steve Kriss --- changelogs/unreleased/4099-skriss-minor.md | 4 + internal/dag/accessors.go | 210 ++++++++---- internal/dag/builder.go | 10 +- internal/dag/builder_test.go | 235 +++++++------- internal/dag/dag.go | 136 +------- internal/dag/dag_test.go | 6 +- internal/dag/extension_processor.go | 2 +- internal/dag/gatewayapi_processor.go | 6 +- internal/dag/httpproxy_processor.go | 8 +- internal/dag/ingress_processor.go | 6 +- internal/dag/listener_processor.go | 66 ++-- internal/debug/dot.go | 150 ++++++--- internal/xdscache/v3/cluster.go | 37 +-- internal/xdscache/v3/cluster_test.go | 6 +- internal/xdscache/v3/endpointstranslator.go | 28 +- internal/xdscache/v3/listener.go | 340 +++++++++----------- internal/xdscache/v3/listener_test.go | 32 +- internal/xdscache/v3/route.go | 291 +++++++---------- internal/xdscache/v3/route_test.go | 6 +- internal/xdscache/v3/secret.go | 45 +-- internal/xdscache/v3/secret_test.go | 6 +- internal/xdscache/v3/visitor_test.go | 230 ------------- 22 files changed, 739 insertions(+), 1121 deletions(-) create mode 100644 changelogs/unreleased/4099-skriss-minor.md delete mode 100644 internal/xdscache/v3/visitor_test.go diff --git a/changelogs/unreleased/4099-skriss-minor.md b/changelogs/unreleased/4099-skriss-minor.md new file mode 100644 index 00000000000..453c00dcc0f --- /dev/null +++ b/changelogs/unreleased/4099-skriss-minor.md @@ -0,0 +1,4 @@ +### Performance improvement for processing configuration + +The performance of Contour's configuration processing has been made more efficient, particularly for clusters with large numbers (i.e. >1k) of HTTPProxies and/or Ingresses. +This means that there should be less of a delay between creating/updating an HTTPProxy/Ingress in Kubernetes, and having it reflected in Envoy's configuration. diff --git a/internal/dag/accessors.go b/internal/dag/accessors.go index 635ec94391f..2477c5d8967 100644 --- a/internal/dag/accessors.go +++ b/internal/dag/accessors.go @@ -18,6 +18,7 @@ import ( "strconv" "github.com/projectcontour/contour/internal/annotation" + "github.com/projectcontour/contour/internal/xds" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -26,7 +27,7 @@ import ( // EnsureService looks for a Kubernetes service in the cache matching the provided // namespace, name and port, and returns a DAG service for it. If a matching service // cannot be found in the cache, an error is returned. -func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) { +func (d *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) { svc, svcPort, err := cache.LookupService(meta, port) if err != nil { return nil, err @@ -108,116 +109,193 @@ func externalName(svc *v1.Service) string { return svc.Spec.ExternalName } -// GetSecureVirtualHosts returns all secure virtual hosts in the DAG. -func (dag *DAG) GetSecureVirtualHosts() map[ListenerName]*SecureVirtualHost { - getter := svhostGetter(map[ListenerName]*SecureVirtualHost{}) - dag.Visit(getter.visit) - return getter -} - // GetSecureVirtualHost returns the secure virtual host in the DAG that // matches the provided name, or nil if no matching secure virtual host // is found. -func (dag *DAG) GetSecureVirtualHost(ln ListenerName) *SecureVirtualHost { - return dag.GetSecureVirtualHosts()[ln] +func (d *DAG) GetSecureVirtualHost(hostname string) *SecureVirtualHost { + return d.SecureVirtualHosts[hostname] } // EnsureSecureVirtualHost adds a secure virtual host with the provided // name to the DAG if it does not already exist, and returns it. -func (dag *DAG) EnsureSecureVirtualHost(ln ListenerName) *SecureVirtualHost { - if svh := dag.GetSecureVirtualHost(ln); svh != nil { +func (d *DAG) EnsureSecureVirtualHost(hostname string) *SecureVirtualHost { + if svh := d.GetSecureVirtualHost(hostname); svh != nil { return svh } svh := &SecureVirtualHost{ VirtualHost: VirtualHost{ - Name: ln.Name, - ListenerName: ln.ListenerName, + Name: hostname, + ListenerName: "ingress_https", }, } - dag.AddRoot(svh) + d.SecureVirtualHosts[hostname] = svh return svh } -// svhostGetter is a visitor that gets all secure virtual hosts -// in the DAG. -type svhostGetter map[ListenerName]*SecureVirtualHost - -func (s svhostGetter) visit(vertex Vertex) { - switch obj := vertex.(type) { - case *SecureVirtualHost: - s[ListenerName{Name: obj.Name, ListenerName: obj.VirtualHost.ListenerName}] = obj - default: - vertex.Visit(s.visit) - } -} - -// GetVirtualHosts returns all virtual hosts in the DAG. -func (dag *DAG) GetVirtualHosts() map[ListenerName]*VirtualHost { - getter := vhostGetter(map[ListenerName]*VirtualHost{}) - dag.Visit(getter.visit) - return getter -} - // GetVirtualHost returns the virtual host in the DAG that matches the // provided name, or nil if no matching virtual host is found. -func (dag *DAG) GetVirtualHost(ln ListenerName) *VirtualHost { - return dag.GetVirtualHosts()[ln] +func (d *DAG) GetVirtualHost(hostname string) *VirtualHost { + return d.VirtualHosts[hostname] } // EnsureVirtualHost adds a virtual host with the provided name to the // DAG if it does not already exist, and returns it. -func (dag *DAG) EnsureVirtualHost(ln ListenerName) *VirtualHost { - if vhost := dag.GetVirtualHost(ln); vhost != nil { +func (d *DAG) EnsureVirtualHost(hostname string) *VirtualHost { + if vhost := d.GetVirtualHost(hostname); vhost != nil { return vhost } vhost := &VirtualHost{ - Name: ln.Name, - ListenerName: ln.ListenerName, + Name: hostname, + ListenerName: "ingress_http", } - dag.AddRoot(vhost) + d.VirtualHosts[hostname] = vhost return vhost } -// vhostGetter is a visitor that gets all virtual hosts -// in the DAG. -type vhostGetter map[ListenerName]*VirtualHost +func (d *DAG) GetClusters() []*Cluster { + var res []*Cluster + + for _, listener := range d.Listeners { + for _, vhost := range listener.VirtualHosts { + for _, route := range vhost.Routes { + res = append(res, route.Clusters...) + + if route.MirrorPolicy != nil && route.MirrorPolicy.Cluster != nil { + res = append(res, route.MirrorPolicy.Cluster) + } + } + } + + for _, vhost := range listener.SecureVirtualHosts { + for _, route := range vhost.Routes { + res = append(res, route.Clusters...) + + if route.MirrorPolicy != nil && route.MirrorPolicy.Cluster != nil { + res = append(res, route.MirrorPolicy.Cluster) + } + } + + if vhost.TCPProxy != nil { + res = append(res, vhost.TCPProxy.Clusters...) + } + } + } + + return res +} -func (v vhostGetter) visit(vertex Vertex) { - switch obj := vertex.(type) { - case *VirtualHost: - v[ListenerName{Name: obj.Name, ListenerName: obj.ListenerName}] = obj - default: - vertex.Visit(v.visit) +func (d *DAG) GetServiceClusters() []*ServiceCluster { + var res []*ServiceCluster + + for _, cluster := range d.GetClusters() { + // A Service has only one WeightedService entry. Fake up a + // ServiceCluster so that the visitor can pretend to not + // know this. + c := &ServiceCluster{ + ClusterName: xds.ClusterLoadAssignmentName( + types.NamespacedName{ + Name: cluster.Upstream.Weighted.ServiceName, + Namespace: cluster.Upstream.Weighted.ServiceNamespace, + }, + cluster.Upstream.Weighted.ServicePort.Name), + Services: []WeightedService{ + cluster.Upstream.Weighted, + }, + } + + res = append(res, c) } + + for _, ec := range d.ExtensionClusters { + res = append(res, &ec.Upstream) + } + + return res } // GetExtensionClusters returns all extension clusters in the DAG. -func (dag *DAG) GetExtensionClusters() map[string]*ExtensionCluster { - getter := extensionClusterGetter(map[string]*ExtensionCluster{}) - dag.Visit(getter.visit) - return getter +func (d *DAG) GetExtensionClusters() map[string]*ExtensionCluster { + // TODO for DAG consumers, this should iterate + // over Listeners.SecureVirtualHosts and get + // AuthorizationServices. + res := map[string]*ExtensionCluster{} + for _, ec := range d.ExtensionClusters { + res[ec.Name] = ec + } + return res +} + +func (d *DAG) GetSecrets() []*Secret { + var res []*Secret + for _, l := range d.Listeners { + for _, svh := range l.SecureVirtualHosts { + if svh.Secret != nil { + res = append(res, svh.Secret) + } + if svh.FallbackCertificate != nil { + res = append(res, svh.FallbackCertificate) + } + } + } + + for _, c := range d.GetClusters() { + if c.ClientCertificate != nil { + res = append(res, c.ClientCertificate) + } + } + + return res } // GetExtensionCluster returns the extension cluster in the DAG that // matches the provided name, or nil if no matching extension cluster // is found. -func (dag *DAG) GetExtensionCluster(name string) *ExtensionCluster { - return dag.GetExtensionClusters()[name] +func (d *DAG) GetExtensionCluster(name string) *ExtensionCluster { + for _, ec := range d.ExtensionClusters { + if ec.Name == name { + return ec + } + } + + return nil } -// extensionClusterGetter is a visitor that gets all extension clusters -// in the DAG. -type extensionClusterGetter map[string]*ExtensionCluster +func (d *DAG) GetVirtualHostRoutes() map[*VirtualHost][]*Route { + res := map[*VirtualHost][]*Route{} + + for _, l := range d.Listeners { + for _, vhost := range l.VirtualHosts { + var routes []*Route + for _, r := range vhost.Routes { + routes = append(routes, r) + } + if len(routes) > 0 { + res[vhost] = routes + } + } + } -func (v extensionClusterGetter) visit(vertex Vertex) { - switch obj := vertex.(type) { - case *ExtensionCluster: - v[obj.Name] = obj - default: - vertex.Visit(v.visit) + return res +} + +func (d *DAG) GetSecureVirtualHostRoutes() map[*SecureVirtualHost][]*Route { + res := map[*SecureVirtualHost][]*Route{} + + for _, l := range d.Listeners { + for _, vhost := range l.SecureVirtualHosts { + var routes []*Route + for _, r := range vhost.Routes { + routes = append(routes, r) + } + if len(routes) > 0 { + res[vhost] = routes + } + } } + + return res } // validSecret returns true if the Secret contains certificate and private key material. diff --git a/internal/dag/builder.go b/internal/dag/builder.go index 4666a5d3ad2..4a1938ec3c1 100644 --- a/internal/dag/builder.go +++ b/internal/dag/builder.go @@ -59,12 +59,14 @@ func (b *Builder) Build() *DAG { gatewayController = b.Source.gatewayclass.Spec.ControllerName } - dag := DAG{ - StatusCache: status.NewCache(gatewayNSName, gatewayController), + dag := &DAG{ + VirtualHosts: map[string]*VirtualHost{}, + SecureVirtualHosts: map[string]*SecureVirtualHost{}, + StatusCache: status.NewCache(gatewayNSName, gatewayController), } for _, p := range b.Processors { - p.Run(&dag, &b.Source) + p.Run(dag, &b.Source) } - return &dag + return dag } diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index f46b75f015a..fa8d40e2151 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -470,7 +470,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { objs []interface{} gatewayclass *gatewayapi_v1alpha2.GatewayClass gateway *gatewayapi_v1alpha2.Gateway - want []Vertex + want []*Listener }{ "insert basic single route, single hostname": { gatewayclass: validClass, @@ -680,7 +680,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -718,7 +718,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -785,7 +785,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -916,7 +916,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -1905,12 +1905,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", ListenerName: "ingress_https", - routes: routes(prefixrouteHTTPRoute("/", service(kuardService))), + Routes: routes(prefixrouteHTTPRoute("/", service(kuardService))), }, Secret: secret(sec1), }, @@ -1955,12 +1955,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", ListenerName: "ingress_https", - routes: routes(prefixrouteHTTPRoute("/", service(kuardService))), + Routes: routes(prefixrouteHTTPRoute("/", service(kuardService))), }, Secret: secret(sec1), }, @@ -2191,12 +2191,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", ListenerName: "ingress_https", - routes: routes(prefixrouteHTTPRoute("/", service(blogService))), + Routes: routes(prefixrouteHTTPRoute("/", service(blogService))), }, Secret: secret(sec1), }, @@ -2915,7 +2915,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3002,7 +3002,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3067,7 +3067,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3306,7 +3306,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "another.projectcontour.io", @@ -3366,7 +3366,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3439,7 +3439,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "*", @@ -3505,7 +3505,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3554,7 +3554,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3603,7 +3603,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "tcp.projectcontour.io", @@ -3712,13 +3712,13 @@ func TestDAGInsertGatewayAPI(t *testing.T) { dag := builder.Build() got := make(map[int]*Listener) - dag.Visit(listenerMap(got).Visit) + for _, l := range dag.Listeners { + got[l.Port] = l + } want := make(map[int]*Listener) for _, v := range tc.want { - if l, ok := v.(*Listener); ok { - want[l.Port] = l - } + want[v.Port] = v } assert.Equal(t, want, got) }) @@ -7166,7 +7166,7 @@ func TestDAGInsert(t *testing.T) { enableExternalNameSvc bool fallbackCertificateName string fallbackCertificateNamespace string - want []Vertex + want []*Listener }{ "ingressv1: insert ingress w/ default backend w/o matching service": { objs: []interface{}{ @@ -7361,7 +7361,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("kuard.example.com", sec1, prefixroute("/", service(s1))), ), }, @@ -7382,7 +7382,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("kuard.example.com", sec3, prefixroute("/", service(s1))), ), }, @@ -7483,7 +7483,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("b.example.com", sec1, prefixroute("/", service(s1))), ), }, @@ -7504,7 +7504,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("b.example.com", sec1, prefixroute("/", service(s1))), ), }, @@ -7564,7 +7564,7 @@ func TestDAGInsert(t *testing.T) { objs: []interface{}{ i9V1, }, - want: []Vertex{}, + want: listeners(), }, "ingressv1: insert ingress w/ two paths httpAllowed: false then tls and service": { objs: []interface{}{ @@ -7575,7 +7575,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("b.example.com", sec1, prefixroute("/", service(s1)), prefixroute("/kuarder", service(s2)), @@ -7588,19 +7588,19 @@ func TestDAGInsert(t *testing.T) { objs: []interface{}{ i1aV1, }, - want: []Vertex{}, + want: listeners(), }, "ingressv1: insert default ingress httpAllowed: false then tls and service": { objs: []interface{}{ i1aV1, sec1, s1, }, - want: []Vertex{}, // default ingress cannot be tls + want: listeners(), // default ingress cannot be tls }, "ingressv1: insert ingress w/ two vhosts httpAllowed: false": { objs: []interface{}{ i6aV1, }, - want: []Vertex{}, + want: listeners(), }, "ingressv1: insert ingress w/ two vhosts httpAllowed: false then tls and service": { objs: []interface{}{ @@ -7609,7 +7609,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("b.example.com", sec1, prefixroute("/", service(s1))), ), }, @@ -7627,7 +7627,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("b.example.com", sec1, routeUpgrade("/", service(s1))), ), }, @@ -7646,7 +7646,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("b.example.com", sec1, routeUpgrade("/", service(s1))), ), }, @@ -7664,12 +7664,12 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "foo.com", ListenerName: "ingress_https", - routes: routes( + Routes: routes( routeUpgrade("/", service(s1)), ), }, @@ -7692,12 +7692,12 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "foo.com", ListenerName: "ingress_https", - routes: routes( + Routes: routes( routeUpgrade("/", service(s1)), ), }, @@ -7720,7 +7720,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("foo.com", sec1, routeUpgrade("/", service(s1))), ), }, @@ -7786,12 +7786,12 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "b.example.com", ListenerName: "ingress_https", - routes: routes( + Routes: routes( prefixroute("/", service(s1)), ), }, @@ -8182,7 +8182,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("example.com", sec13, routeUpgrade("/", service(s13a)), prefixroute("/.well-known/acme-challenge/gVJl5NWL2owUqZekjHkt_bo3OHYC2XNDURRRgLI5JTk", service(s13b)), @@ -8618,7 +8618,7 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( securevirtualhost("foo.com", sec1, routeUpgrade("/", service(s1))), ), }, @@ -8771,12 +8771,12 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes( + Routes: routes( routeUpgrade("/", service(s1))), }, MinTLSVersion: "1.2", @@ -8796,7 +8796,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", @@ -8829,12 +8829,12 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes( + Routes: routes( routeUpgrade("/", service(s1))), }, MinTLSVersion: "1.2", @@ -8859,12 +8859,12 @@ func TestDAGInsert(t *testing.T) { ), }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes( + Routes: routes( routeUpgrade("/", service(s1))), }, MinTLSVersion: "1.2", @@ -8901,7 +8901,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "www.example.com", // this is proxy39, not proxy38 @@ -8922,7 +8922,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "www.example.com", @@ -8944,7 +8944,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "www.example.com", @@ -8965,7 +8965,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "passthrough.example.com", @@ -9350,7 +9350,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "kuard.example.com", @@ -9382,7 +9382,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "kuard.example.com", @@ -9424,7 +9424,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "kuard.example.com", @@ -9512,7 +9512,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", @@ -9571,7 +9571,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", @@ -9629,7 +9629,7 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", @@ -9735,7 +9735,7 @@ func TestDAGInsert(t *testing.T) { want: listeners( &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", @@ -10158,12 +10158,12 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10254,12 +10254,12 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10319,12 +10319,12 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10457,12 +10457,12 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10472,7 +10472,7 @@ func TestDAGInsert(t *testing.T) { VirtualHost: VirtualHost{ Name: "projectcontour.io", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10519,12 +10519,12 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10572,12 +10572,12 @@ func TestDAGInsert(t *testing.T) { }, &Listener{ Port: 443, - VirtualHosts: virtualhosts( + SecureVirtualHosts: securevirtualhosts( &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "example.com", ListenerName: "ingress_https", - routes: routes(routeUpgrade("/", service(s9))), + Routes: routes(routeUpgrade("/", service(s9))), }, MinTLSVersion: "1.2", Secret: secret(sec1), @@ -10619,27 +10619,19 @@ func TestDAGInsert(t *testing.T) { dag := builder.Build() got := make(map[int]*Listener) - dag.Visit(listenerMap(got).Visit) + for _, l := range dag.Listeners { + got[l.Port] = l + } want := make(map[int]*Listener) - for _, v := range tc.want { - if l, ok := v.(*Listener); ok { - want[l.Port] = l - } + for _, l := range tc.want { + want[l.Port] = l } assert.Equal(t, want, got) }) } } -type listenerMap map[int]*Listener - -func (lm listenerMap) Visit(v Vertex) { - if l, ok := v.(*Listener); ok { - lm[l.Port] = l - } -} - func backendv1(name string, port intstr.IntOrString) *networking_v1.IngressBackend { var v1port networking_v1.ServiceBackendPort @@ -10805,16 +10797,7 @@ func TestDAGRootNamespaces(t *testing.T) { } dag := builder.Build() - var count int - dag.Visit(func(v Vertex) { - v.Visit(func(v Vertex) { - if _, ok := v.(*VirtualHost); ok { - count++ - } - }) - }) - - if tc.want != count { + if count := len(dag.VirtualHosts); tc.want != count { t.Errorf("wanted %d vertices, but got %d", tc.want, count) } }) @@ -11257,7 +11240,7 @@ func TestBuilderRunsProcessorsInOrder(t *testing.T) { func TestHTTPProxyConficts(t *testing.T) { type testcase struct { objs []interface{} - wantListeners []Vertex + wantListeners []*Listener wantStatus map[types.NamespacedName]contour_api_v1.DetailedCondition } @@ -11280,13 +11263,13 @@ func TestHTTPProxyConficts(t *testing.T) { dag := builder.Build() gotListeners := make(map[int]*Listener) - dag.Visit(listenerMap(gotListeners).Visit) + for _, l := range dag.Listeners { + gotListeners[l.Port] = l + } want := make(map[int]*Listener) - for _, v := range tc.wantListeners { - if l, ok := v.(*Listener); ok { - want[l.Port] = l - } + for _, l := range tc.wantListeners { + want[l.Port] = l } assert.Equal(t, want, gotListeners) @@ -11681,16 +11664,16 @@ func TestHTTPProxyConficts(t *testing.T) { }, existingService1, }, - wantListeners: listeners( - &Listener{ + wantListeners: []*Listener{ + { Port: 80, - VirtualHosts: virtualhosts( + VirtualHosts: []*VirtualHost{ virtualhost("example.com", directResponseRoute("/missing", http.StatusServiceUnavailable), prefixroute("/existing", service(existingService1))), - ), + }, }, - ), + }, wantStatus: map[types.NamespacedName]contour_api_v1.DetailedCondition{ {Name: "invalid-child-proxy", Namespace: "default"}: fixture.NewValidCondition(). WithError(contour_api_v1.ConditionTypeServiceError, "ServiceUnresolvedReference", `Spec.Routes unresolved service reference: service "default/missing-service" not found`), @@ -11772,7 +11755,7 @@ func TestDefaultHeadersPolicies(t *testing.T) { tests := []struct { name string objs []interface{} - want []Vertex + want []*Listener ingressReqHp *HeadersPolicy ingressRespHp *HeadersPolicy httpProxyReqHp *HeadersPolicy @@ -11884,13 +11867,13 @@ func TestDefaultHeadersPolicies(t *testing.T) { dag := builder.Build() got := make(map[int]*Listener) - dag.Visit(listenerMap(got).Visit) + for _, l := range dag.Listeners { + got[l.Port] = l + } want := make(map[int]*Listener) - for _, v := range tc.want { - if l, ok := v.(*Listener); ok { - want[l.Port] = l - } + for _, l := range tc.want { + want[l.Port] = l } assert.Equal(t, want, got) }) @@ -12092,7 +12075,11 @@ func secret(s *v1.Secret) *Secret { } } -func virtualhosts(vx ...Vertex) []Vertex { +func virtualhosts(vx ...*VirtualHost) []*VirtualHost { + return vx +} + +func securevirtualhosts(vx ...*SecureVirtualHost) []*SecureVirtualHost { return vx } @@ -12100,7 +12087,7 @@ func virtualhost(name string, first *Route, rest ...*Route) *VirtualHost { return &VirtualHost{ Name: name, ListenerName: "ingress_http", - routes: routes(append([]*Route{first}, rest...)...), + Routes: routes(append([]*Route{first}, rest...)...), } } @@ -12109,18 +12096,16 @@ func securevirtualhost(name string, sec *v1.Secret, first *Route, rest ...*Route VirtualHost: VirtualHost{ Name: name, ListenerName: "ingress_https", - routes: routes(append([]*Route{first}, rest...)...), + Routes: routes(append([]*Route{first}, rest...)...), }, MinTLSVersion: "1.2", Secret: secret(sec), } } -func listeners(ls ...*Listener) []Vertex { - var v []Vertex - for _, l := range ls { - v = append(v, l) - } +func listeners(ls ...*Listener) []*Listener { + var v []*Listener + v = append(v, ls...) return v } diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 13ad4236d8d..feb817bcc1f 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -25,16 +25,10 @@ import ( "github.com/projectcontour/contour/internal/status" "github.com/projectcontour/contour/internal/timeout" - "github.com/projectcontour/contour/internal/xds" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) -// Vertex is a node in the DAG that can be visited. -type Vertex interface { - Visit(func(Vertex)) -} - // Observer is an interface for receiving notification of DAG updates. type Observer interface { OnChange(*DAG) @@ -61,42 +55,14 @@ func ComposeObservers(observers ...Observer) Observer { }) } -// A DAG represents a directed acyclic graph of objects representing the relationship -// between Kubernetes Ingress objects, the backend Services, and Secret objects. -// The DAG models these relationships as Roots and Vertices. type DAG struct { // StatusCache holds a cache of status updates to send. StatusCache status.Cache - // roots are the root vertices of this DAG. - roots []Vertex -} - -// Visit calls fn on each root of this DAG. -func (d *DAG) Visit(fn func(Vertex)) { - for _, r := range d.roots { - fn(r) - } -} - -// AddRoot appends the given root to the DAG's roots. -func (d *DAG) AddRoot(root Vertex) { - d.roots = append(d.roots, root) -} - -// RemoveRoot removes the given root from the DAG's roots if it exists. -func (d *DAG) RemoveRoot(root Vertex) { - idx := -1 - for i := range d.roots { - if d.roots[i] == root { - idx = i - break - } - } - - if idx >= 0 { - d.roots = append(d.roots[:idx], d.roots[idx+1:]...) - } + Listeners []*Listener + VirtualHosts map[string]*VirtualHost + SecureVirtualHosts map[string]*SecureVirtualHost + ExtensionClusters []*ExtensionCluster } type MatchCondition interface { @@ -497,17 +463,6 @@ func (pvc *PeerValidationContext) GetSubjectName() string { return pvc.SubjectName } -func (r *Route) Visit(f func(Vertex)) { - for _, c := range r.Clusters { - f(c) - } - // Allow any mirror clusters to also be visited so that - // they are also added to CDS. - if r.MirrorPolicy != nil && r.MirrorPolicy.Cluster != nil { - f(r.MirrorPolicy.Cluster) - } -} - // A VirtualHost represents a named L4/L7 service. type VirtualHost struct { // Name is the fully qualified domain name of a network host, @@ -523,14 +478,14 @@ type VirtualHost struct { // are rate limited. RateLimitPolicy *RateLimitPolicy - routes map[string]*Route + Routes map[string]*Route } func (v *VirtualHost) addRoute(route *Route) { - if v.routes == nil { - v.routes = make(map[string]*Route) + if v.Routes == nil { + v.Routes = make(map[string]*Route) } - v.routes[conditionsToString(route)] = route + v.Routes[conditionsToString(route)] = route } func conditionsToString(r *Route) string { @@ -541,15 +496,9 @@ func conditionsToString(r *Route) string { return strings.Join(s, ",") } -func (v *VirtualHost) Visit(f func(Vertex)) { - for _, r := range v.routes { - f(r) - } -} - func (v *VirtualHost) Valid() bool { // A VirtualHost is valid if it has at least one route. - return len(v.routes) > 0 + return len(v.Routes) > 0 } // A SecureVirtualHost represents a HTTP host protected by TLS. @@ -587,21 +536,11 @@ type SecureVirtualHost struct { AuthorizationFailOpen bool } -func (s *SecureVirtualHost) Visit(f func(Vertex)) { - s.VirtualHost.Visit(f) - if s.TCPProxy != nil { - f(s.TCPProxy) - } - if s.Secret != nil { - f(s.Secret) // secret is not required if vhost is using tls passthrough - } -} - func (s *SecureVirtualHost) Valid() bool { // A SecureVirtualHost is valid if either // 1. it has a secret and at least one route. // 2. it has a tcpproxy, because the tcpproxy backend may negotiate TLS itself. - return (s.Secret != nil && len(s.routes) > 0) || s.TCPProxy != nil + return (s.Secret != nil && len(s.Routes) > 0) || s.TCPProxy != nil } type ListenerName struct { @@ -612,6 +551,7 @@ type ListenerName struct { // A Listener represents a TCP socket that accepts // incoming connections. type Listener struct { + Name string // Address is the TCP address to listen on. // If blank 0.0.0.0, or ::/0 for IPv6, is assumed. @@ -620,13 +560,8 @@ type Listener struct { // Port is the TCP port to listen on. Port int - VirtualHosts []Vertex -} - -func (l *Listener) Visit(f func(Vertex)) { - for _, vh := range l.VirtualHosts { - f(vh) - } + VirtualHosts []*VirtualHost + SecureVirtualHosts []*SecureVirtualHost } // TCPProxy represents a cluster of TCP endpoints. @@ -637,12 +572,6 @@ type TCPProxy struct { Clusters []*Cluster } -func (t *TCPProxy) Visit(f func(Vertex)) { - for _, s := range t.Clusters { - f(s) - } -} - // Service represents a single Kubernetes' Service's Port. type Service struct { Weighted WeightedService @@ -673,26 +602,6 @@ type Service struct { ExternalName string } -// Visit applies the visitor function to the Service vertex. -func (s *Service) Visit(f func(Vertex)) { - // A Service has only one WeightedService entry. Fake up a - // ServiceCluster so that the visitor can pretend to not - // know this. - c := ServiceCluster{ - ClusterName: xds.ClusterLoadAssignmentName( - types.NamespacedName{ - Name: s.Weighted.ServiceName, - Namespace: s.Weighted.ServiceNamespace, - }, - s.Weighted.ServicePort.Name), - Services: []WeightedService{ - s.Weighted, - }, - } - - f(&c) -} - // Cluster holds the connection specific parameters that apply to // traffic routed to an upstream service. type Cluster struct { @@ -751,10 +660,6 @@ type Cluster struct { ClientCertificate *Secret } -func (c Cluster) Visit(f func(Vertex)) { - f(c.Upstream) -} - // WeightedService represents the load balancing weight of a // particular v1.Weighted port. type WeightedService struct { @@ -819,10 +724,6 @@ func (s *ServiceCluster) Validate() error { return nil } -func (s *ServiceCluster) Visit(func(Vertex)) { - // ServiceClusters are leaves in the DAG. -} - // AddService adds the given service with a default weight of 1. func (s *ServiceCluster) AddService(name types.NamespacedName, port v1.ServicePort) { s.AddWeightedService(1, name, port) @@ -864,9 +765,8 @@ type Secret struct { Object *v1.Secret } -func (s *Secret) Name() string { return s.Object.Name } -func (s *Secret) Namespace() string { return s.Object.Namespace } -func (s *Secret) Visit(func(Vertex)) {} +func (s *Secret) Name() string { return s.Object.Name } +func (s *Secret) Namespace() string { return s.Object.Namespace } // Data returns the contents of the backing secret's map. func (s *Secret) Data() map[string][]byte { @@ -931,12 +831,6 @@ type ExtensionCluster struct { ClientCertificate *Secret } -// Visit processes extension clusters. -func (e *ExtensionCluster) Visit(f func(Vertex)) { - // Emit the upstream ServiceCluster to the visitor. - f(&e.Upstream) -} - func wildcardDomainHeaderMatch(fqdn string) HeaderMatchCondition { return HeaderMatchCondition{ // Internally Envoy uses the HTTP/2 ":authority" header in diff --git a/internal/dag/dag_test.go b/internal/dag/dag_test.go index e552db49c4f..50b3bfc56c2 100644 --- a/internal/dag/dag_test.go +++ b/internal/dag/dag_test.go @@ -27,7 +27,7 @@ func TestVirtualHostValid(t *testing.T) { assert.False(t, vh.Valid()) vh = VirtualHost{ - routes: map[string]*Route{ + Routes: map[string]*Route{ "/": {}, }, } @@ -46,7 +46,7 @@ func TestSecureVirtualHostValid(t *testing.T) { vh = SecureVirtualHost{ VirtualHost: VirtualHost{ - routes: map[string]*Route{ + Routes: map[string]*Route{ "/": {}, }, }, @@ -56,7 +56,7 @@ func TestSecureVirtualHostValid(t *testing.T) { vh = SecureVirtualHost{ Secret: new(Secret), VirtualHost: VirtualHost{ - routes: map[string]*Route{ + Routes: map[string]*Route{ "/": {}, }, }, diff --git a/internal/dag/extension_processor.go b/internal/dag/extension_processor.go index 686beba37ce..3342af20580 100644 --- a/internal/dag/extension_processor.go +++ b/internal/dag/extension_processor.go @@ -46,7 +46,7 @@ func (p *ExtensionServiceProcessor) Run(dag *DAG, cache *KubernetesCache) { if ext := p.buildExtensionService(cache, e, validCondition); ext != nil { if len(validCondition.Errors) == 0 { - dag.AddRoot(ext) + dag.ExtensionClusters = append(dag.ExtensionClusters, ext) } } diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index d01a44a18d5..95fffe47203 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -539,7 +539,7 @@ func (p *GatewayAPIProcessor) computeTLSRoute(route *gatewayapi_v1alpha2.TLSRout } for host := range hosts { - secure := p.dag.EnsureSecureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_https"}) + secure := p.dag.EnsureSecureVirtualHost(host) if listenerSecret != nil { secure.Secret = listenerSecret @@ -673,11 +673,11 @@ func (p *GatewayAPIProcessor) computeHTTPRoute(route *gatewayapi_v1alpha2.HTTPRo switch { case listenerSecret != nil: - svhost := p.dag.EnsureSecureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_https"}) + svhost := p.dag.EnsureSecureVirtualHost(host) svhost.Secret = listenerSecret svhost.addRoute(route) default: - vhost := p.dag.EnsureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_http"}) + vhost := p.dag.EnsureVirtualHost(host) vhost.addRoute(route) } } diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index b5e1c007a32..4db2b6dfd08 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -190,7 +190,7 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { return } - svhost := p.dag.EnsureSecureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_https"}) + svhost := p.dag.EnsureSecureVirtualHost(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") @@ -313,7 +313,7 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { } routes := p.computeRoutes(validCond, proxy, proxy, nil, nil, tlsEnabled) - insecure := p.dag.EnsureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_http"}) + insecure := p.dag.EnsureVirtualHost(host) cp, err := toCORSPolicy(proxy.Spec.VirtualHost.CORSPolicy) if err != nil { validCond.AddErrorf(contour_api_v1.ConditionTypeCORSError, "PolicyDidNotParse", @@ -335,7 +335,7 @@ 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(ListenerName{Name: host, ListenerName: "ingress_https"}) + secure := p.dag.EnsureSecureVirtualHost(host) secure.CORSPolicy = cp rlp, err := rateLimitPolicy(proxy.Spec.VirtualHost.RateLimitPolicy) @@ -766,7 +766,7 @@ func (p *HTTPProxyProcessor) processHTTPProxyTCPProxy(validCond *contour_api_v1. SNI: s.ExternalName, }) } - secure := p.dag.EnsureSecureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_https"}) + secure := p.dag.EnsureSecureVirtualHost(host) secure.TCPProxy = &proxy return true diff --git a/internal/dag/ingress_processor.go b/internal/dag/ingress_processor.go index 1d82024b09f..8308973f93e 100644 --- a/internal/dag/ingress_processor.go +++ b/internal/dag/ingress_processor.go @@ -99,7 +99,7 @@ func (p *IngressProcessor) computeSecureVirtualhosts() { // ahead and create the SecureVirtualHost for this // Ingress. for _, host := range tls.Hosts { - svhost := p.dag.EnsureSecureVirtualHost(ListenerName{Name: host, ListenerName: "ingress_https"}) + svhost := p.dag.EnsureSecureVirtualHost(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") @@ -178,14 +178,14 @@ 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(ListenerName{Name: host, ListenerName: "ingress_http"}) + vhost := p.dag.EnsureVirtualHost(host) vhost.addRoute(r) } // 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(ListenerName{Name: host, ListenerName: "ingress_https"}); svh != nil && host != "*" { + if svh := p.dag.GetSecureVirtualHost(host); svh != nil && host != "*" { svh.addRoute(r) } } diff --git a/internal/dag/listener_processor.go b/internal/dag/listener_processor.go index f8369f9a2fc..408fa8a2656 100644 --- a/internal/dag/listener_processor.go +++ b/internal/dag/listener_processor.go @@ -29,77 +29,53 @@ func (p *ListenerProcessor) Run(dag *DAG, _ *KubernetesCache) { } // buildHTTPListener builds a *dag.Listener for the vhosts bound to port 80. -// The list of virtual hosts will attached to the listener will be sorted -// by hostname. +// The list of virtual hosts attached to the listener will be sorted by hostname. func (p *ListenerProcessor) buildHTTPListener(dag *DAG) { - var virtualhosts []Vertex - var remove []Vertex - - for _, root := range dag.roots { - if obj, ok := root.(*VirtualHost); ok { - remove = append(remove, obj) - - if obj.Valid() { - virtualhosts = append(virtualhosts, obj) - } + var vhosts []*VirtualHost + for _, vh := range dag.VirtualHosts { + if vh.Valid() { + vhosts = append(vhosts, vh) } } - // Update the DAG's roots to not include virtual hosts. - for _, r := range remove { - dag.RemoveRoot(r) - } - - if len(virtualhosts) == 0 { + if len(vhosts) == 0 { return } - sort.SliceStable(virtualhosts, func(i, j int) bool { - return virtualhosts[i].(*VirtualHost).Name < virtualhosts[j].(*VirtualHost).Name + sort.SliceStable(vhosts, func(i, j int) bool { + return vhosts[i].Name < vhosts[j].Name }) http := &Listener{ Port: 80, - VirtualHosts: virtualhosts, + VirtualHosts: vhosts, } - dag.AddRoot(http) + dag.Listeners = append(dag.Listeners, http) } // buildHTTPSListener builds a *dag.Listener for the vhosts bound to port 443. -// The list of virtual hosts will attached to the listener will be sorted -// by hostname. +// The list of virtual hosts attached to the listener will be sorted by hostname. func (p *ListenerProcessor) buildHTTPSListener(dag *DAG) { - var virtualhosts []Vertex - var remove []Vertex - - for _, root := range dag.roots { - if obj, ok := root.(*SecureVirtualHost); ok { - remove = append(remove, obj) - - if obj.Valid() { - virtualhosts = append(virtualhosts, obj) - } + var vhosts []*SecureVirtualHost + for _, svh := range dag.SecureVirtualHosts { + if svh.Valid() { + vhosts = append(vhosts, svh) } } - // Update the DAG's roots to not include secure virtual hosts. - for _, r := range remove { - dag.RemoveRoot(r) - } - - if len(virtualhosts) == 0 { + if len(vhosts) == 0 { return } - sort.SliceStable(virtualhosts, func(i, j int) bool { - return virtualhosts[i].(*SecureVirtualHost).Name < virtualhosts[j].(*SecureVirtualHost).Name + sort.SliceStable(vhosts, func(i, j int) bool { + return vhosts[i].Name < vhosts[j].Name }) https := &Listener{ - Port: 443, - VirtualHosts: virtualhosts, + Port: 443, + SecureVirtualHosts: vhosts, } - dag.AddRoot(https) + dag.Listeners = append(dag.Listeners, https) } diff --git a/internal/debug/dot.go b/internal/debug/dot.go index 08597a667cc..74e4d72ff41 100644 --- a/internal/debug/dot.go +++ b/internal/debug/dot.go @@ -31,65 +31,113 @@ type pair struct { a, b interface{} } -type ctx struct { - w io.Writer - nodes map[interface{}]bool - edges map[pair]bool -} +func (dw *dotWriter) writeDot(w io.Writer) { + fmt.Fprintln(w, "digraph DAG {\nrankdir=\"LR\"") -func (c *ctx) writeVertex(v dag.Vertex) { - if c.nodes[v] { - return - } - c.nodes[v] = true - switch v := v.(type) { - case *dag.Listener: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{listener|%s:%d}"]`+"\n", v, v.Address, v.Port) - case *dag.Secret: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{secret|%s/%s}"]`+"\n", v, v.Namespace(), v.Name()) - case *dag.Service: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{service|%s/%s:%d}"]`+"\n", - v, v.Weighted.ServiceNamespace, v.Weighted.ServiceName, v.Weighted.ServicePort.Port) - case *dag.VirtualHost: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{http://%s}"]`+"\n", v, v.Name) - case *dag.SecureVirtualHost: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{https://%s}"]`+"\n", v, v.VirtualHost.Name) - case *dag.Route: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{%s}"]`+"\n", v, v.PathMatchCondition.String()) - case *dag.TCPProxy: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{tcpproxy}"]`+"\n", v) - case *dag.Cluster: - fmt.Fprintf(c.w, `"%p" [shape=record, label="{cluster|{%s|weight %d}}"]`+"\n", v, envoy.Clustername(v), v.Weight) - } -} + nodes := map[interface{}]bool{} + edges := map[pair]bool{} -func (c *ctx) writeEdge(parent, child dag.Vertex) { - if c.edges[pair{parent, child}] { - return - } - c.edges[pair{parent, child}] = true - fmt.Fprintf(c.w, `"%p" -> "%p"`+"\n", parent, child) -} + // collect nodes and edges + for _, listener := range dw.Builder.Build().Listeners { + nodes[listener] = true -func (dw *dotWriter) writeDot(w io.Writer) { - fmt.Fprintln(w, "digraph DAG {\nrankdir=\"LR\"") + for _, vhost := range listener.VirtualHosts { + edges[pair{listener, vhost}] = true + nodes[vhost] = true + + for _, route := range vhost.Routes { + edges[pair{vhost, route}] = true + nodes[route] = true - ctx := &ctx{ - w: w, - nodes: make(map[interface{}]bool), - edges: make(map[pair]bool), + clusters := route.Clusters + if route.MirrorPolicy != nil && route.MirrorPolicy.Cluster != nil { + clusters = append(clusters, route.MirrorPolicy.Cluster) + } + for _, cluster := range clusters { + edges[pair{route, cluster}] = true + nodes[cluster] = true + + if service := cluster.Upstream; service != nil { + edges[pair{cluster, service}] = true + nodes[service] = true + } + } + } + } + + for _, vhost := range listener.SecureVirtualHosts { + edges[pair{listener, vhost}] = true + nodes[vhost] = true + + for _, route := range vhost.Routes { + edges[pair{vhost, route}] = true + nodes[route] = true + + clusters := route.Clusters + if route.MirrorPolicy != nil && route.MirrorPolicy.Cluster != nil { + clusters = append(clusters, route.MirrorPolicy.Cluster) + } + for _, cluster := range clusters { + edges[pair{route, cluster}] = true + nodes[cluster] = true + + if service := cluster.Upstream; service != nil { + edges[pair{cluster, service}] = true + nodes[service] = true + } + } + } + + if vhost.TCPProxy != nil { + edges[pair{vhost, vhost.TCPProxy}] = true + nodes[vhost.TCPProxy] = true + + for _, cluster := range vhost.TCPProxy.Clusters { + edges[pair{vhost.TCPProxy, cluster}] = true + nodes[cluster] = true + + if service := cluster.Upstream; service != nil { + edges[pair{cluster, service}] = true + nodes[service] = true + } + } + } + + if vhost.Secret != nil { + edges[pair{vhost, vhost.Secret}] = true + nodes[vhost.Secret] = true + } + } } - var visit func(dag.Vertex) - visit = func(parent dag.Vertex) { - ctx.writeVertex(parent) - parent.Visit(func(child dag.Vertex) { - visit(child) - ctx.writeEdge(parent, child) - }) + // print nodes + for node := range nodes { + switch node := node.(type) { + case *dag.Listener: + fmt.Fprintf(w, `"%p" [shape=record, label="{listener|%s:%d}"]`+"\n", node, node.Address, node.Port) + case *dag.VirtualHost: + fmt.Fprintf(w, `"%p" [shape=record, label="{http://%s}"]`+"\n", node, node.Name) + case *dag.SecureVirtualHost: + fmt.Fprintf(w, `"%p" [shape=record, label="{https://%s}"]`+"\n", node, node.VirtualHost.Name) + case *dag.Route: + fmt.Fprintf(w, `"%p" [shape=record, label="{%s}"]`+"\n", node, node.PathMatchCondition.String()) + case *dag.Cluster: + fmt.Fprintf(w, `"%p" [shape=record, label="{cluster|{%s|weight %d}}"]`+"\n", node, envoy.Clustername(node), node.Weight) + case *dag.Service: + fmt.Fprintf(w, `"%p" [shape=record, label="{service|%s/%s:%d}"]`+"\n", + node, node.Weighted.ServiceNamespace, node.Weighted.ServiceName, node.Weighted.ServicePort.Port) + case *dag.Secret: + fmt.Fprintf(w, `"%p" [shape=record, label="{secret|%s/%s}"]`+"\n", node, node.Namespace(), node.Name()) + case *dag.TCPProxy: + fmt.Fprintf(w, `"%p" [shape=record, label="{tcpproxy}"]`+"\n", node) + + } } - dw.Builder.Build().Visit(visit) + // print edges + for edge := range edges { + fmt.Fprintf(w, `"%p" -> "%p"`+"\n", edge.a, edge.b) + } fmt.Fprintln(w, "}") } diff --git a/internal/xdscache/v3/cluster.go b/internal/xdscache/v3/cluster.go index a62b523da03..a0be7eb1dde 100644 --- a/internal/xdscache/v3/cluster.go +++ b/internal/xdscache/v3/cluster.go @@ -77,37 +77,20 @@ func (c *ClusterCache) Query(names []string) []proto.Message { func (*ClusterCache) TypeURL() string { return resource.ClusterType } func (c *ClusterCache) OnChange(root *dag.DAG) { - clusters := visitClusters(root) - c.Update(clusters) -} - -type clusterVisitor struct { - clusters map[string]*envoy_cluster_v3.Cluster -} - -// visitCluster produces a map of *envoy_cluster_v3.Clusters. -func visitClusters(root dag.Vertex) map[string]*envoy_cluster_v3.Cluster { - cv := clusterVisitor{ - clusters: make(map[string]*envoy_cluster_v3.Cluster), - } - cv.visit(root) - return cv.clusters -} + clusters := map[string]*envoy_cluster_v3.Cluster{} -func (v *clusterVisitor) visit(vertex dag.Vertex) { - switch cluster := vertex.(type) { - case *dag.Cluster: + for _, cluster := range root.GetClusters() { name := envoy.Clustername(cluster) - if _, ok := v.clusters[name]; !ok { - v.clusters[name] = envoy_v3.Cluster(cluster) + if _, ok := clusters[name]; !ok { + clusters[name] = envoy_v3.Cluster(cluster) } - case *dag.ExtensionCluster: - name := cluster.Name - if _, ok := v.clusters[name]; !ok { - v.clusters[name] = envoy_v3.ExtensionCluster(cluster) + } + + for name, ec := range root.GetExtensionClusters() { + if _, ok := clusters[name]; !ok { + clusters[name] = envoy_v3.ExtensionCluster(ec) } } - // recurse into children of v - vertex.Visit(v.visit) + c.Update(clusters) } diff --git a/internal/xdscache/v3/cluster_test.go b/internal/xdscache/v3/cluster_test.go index e7d62cde297..215f45407c2 100644 --- a/internal/xdscache/v3/cluster_test.go +++ b/internal/xdscache/v3/cluster_test.go @@ -818,9 +818,9 @@ func TestClusterVisit(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - root := buildDAG(t, tc.objs...) - got := visitClusters(root) - protobuf.ExpectEqual(t, tc.want, got) + var cc ClusterCache + cc.OnChange(buildDAG(t, tc.objs...)) + protobuf.ExpectEqual(t, tc.want, cc.values) }) } } diff --git a/internal/xdscache/v3/endpointstranslator.go b/internal/xdscache/v3/endpointstranslator.go index 7a57608c475..02ae1cf1eed 100644 --- a/internal/xdscache/v3/endpointstranslator.go +++ b/internal/xdscache/v3/endpointstranslator.go @@ -269,30 +269,22 @@ func (e *EndpointsTranslator) Merge(entries map[string]*envoy_endpoint_v3.Cluste } // OnChange observes DAG rebuild events. -func (e *EndpointsTranslator) OnChange(d *dag.DAG) { +func (e *EndpointsTranslator) OnChange(root *dag.DAG) { clusters := []*dag.ServiceCluster{} names := map[string]bool{} - var visitor func(dag.Vertex) - visitor = func(vertex dag.Vertex) { - if svc, ok := vertex.(*dag.ServiceCluster); ok { - if err := svc.Validate(); err != nil { - e.WithError(err).Errorf("dropping invalid service cluster %q", svc.ClusterName) - } else if _, ok := names[svc.ClusterName]; ok { - e.Debugf("dropping service cluster with duplicate name %q", svc.ClusterName) - } else { - e.Debugf("added ServiceCluster %q from DAG", svc.ClusterName) - clusters = append(clusters, svc.DeepCopy()) - names[svc.ClusterName] = true - } + for _, svc := range root.GetServiceClusters() { + if err := svc.Validate(); err != nil { + e.WithError(err).Errorf("dropping invalid service cluster %q", svc.ClusterName) + } else if _, ok := names[svc.ClusterName]; ok { + e.Debugf("dropping service cluster with duplicate name %q", svc.ClusterName) + } else { + e.Debugf("added ServiceCluster %q from DAG", svc.ClusterName) + clusters = append(clusters, svc.DeepCopy()) + names[svc.ClusterName] = true } - - vertex.Visit(visitor) } - // Collect all the service clusters from the DAG. - d.Visit(visitor) - // Update the cache with the new clusters. if err := e.cache.SetClusters(clusters); err != nil { e.WithError(err).Error("failed to cache service clusters") diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index 27776d73a5a..0fdcd3845d9 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -18,8 +18,6 @@ import ( "sort" "sync" - "github.com/sirupsen/logrus" - envoy_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" @@ -34,6 +32,7 @@ import ( "github.com/projectcontour/contour/internal/sorter" "github.com/projectcontour/contour/internal/timeout" "github.com/projectcontour/contour/pkg/config" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/types" ) @@ -476,68 +475,177 @@ func (c *ListenerCache) Query(names []string) []proto.Message { func (*ListenerCache) TypeURL() string { return resource.ListenerType } func (c *ListenerCache) OnChange(root *dag.DAG) { - listeners := visitListeners(root, &c.Config) - c.Update(listeners) -} + cfg := c.Config.DefaultListeners() + listeners := c.Config.SecureListeners() -type listenerVisitor struct { - *ListenerConfig + max := func(a, b envoy_tls_v3.TlsParameters_TlsProtocol) envoy_tls_v3.TlsParameters_TlsProtocol { + if a > b { + return a + } + return b + } - listeners map[string]*envoy_listener_v3.Listener - httpListenerName string // Name of dag.VirtualHost encountered. -} + // need to iterate through Listeners here because we only + // want the vhosts that have been attached to a listener + // by the listener processor. + for _, listener := range root.Listeners { + if len(listener.VirtualHosts) > 0 { + if httpListener, ok := cfg.HTTPListeners[listener.VirtualHosts[0].ListenerName]; ok { + // Add a listener if there are vhosts bound to http. + cm := envoy_v3.HTTPConnectionManagerBuilder(). + Codec(envoy_v3.CodecForVersions(cfg.DefaultHTTPVersions...)). + DefaultFilters(). + RouteConfigName(httpListener.Name). + MetricsPrefix(httpListener.Name). + AccessLoggers(cfg.newInsecureAccessLog()). + RequestTimeout(cfg.RequestTimeout). + ConnectionIdleTimeout(cfg.ConnectionIdleTimeout). + StreamIdleTimeout(cfg.StreamIdleTimeout). + DelayedCloseTimeout(cfg.DelayedCloseTimeout). + MaxConnectionDuration(cfg.MaxConnectionDuration). + ConnectionShutdownGracePeriod(cfg.ConnectionShutdownGracePeriod). + AllowChunkedLength(cfg.AllowChunkedLength). + AddFilter(envoy_v3.OriginalIPDetectionFilter(cfg.XffNumTrustedHops)). + AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). + Get() + + listeners[httpListener.Name] = envoy_v3.Listener( + httpListener.Name, + httpListener.Address, + httpListener.Port, + proxyProtocol(cfg.UseProxyProto), + cm, + ) + } + } -func visitListeners(root dag.Vertex, lvc *ListenerConfig) map[string]*envoy_listener_v3.Listener { - lv := listenerVisitor{ - ListenerConfig: lvc.DefaultListeners(), - listeners: lvc.SecureListeners(), - } + for _, vh := range listener.SecureVirtualHosts { + var alpnProtos []string + var filters []*envoy_listener_v3.Filter + + if vh.TCPProxy == nil { + var authFilter *http.HttpFilter + + if vh.AuthorizationService != nil { + authFilter = envoy_v3.FilterExternalAuthz( + vh.AuthorizationService.Name, + vh.AuthorizationFailOpen, + vh.AuthorizationResponseTimeout, + ) + } + + // Create a uniquely named HTTP connection manager for + // this vhost, so that the SNI name the client requests + // only grants access to that host. See RFC 6066 for + // security advice. Note that we still use the generic + // metrics prefix to keep compatibility with previous + // Contour versions since the metrics prefix will be + // coded into monitoring dashboards. + cm := envoy_v3.HTTPConnectionManagerBuilder(). + Codec(envoy_v3.CodecForVersions(cfg.DefaultHTTPVersions...)). + AddFilter(envoy_v3.FilterMisdirectedRequests(vh.VirtualHost.Name)). + DefaultFilters(). + AddFilter(authFilter). + RouteConfigName(path.Join("https", vh.VirtualHost.Name)). + MetricsPrefix(vh.ListenerName). + AccessLoggers(cfg.newSecureAccessLog()). + RequestTimeout(cfg.RequestTimeout). + ConnectionIdleTimeout(cfg.ConnectionIdleTimeout). + StreamIdleTimeout(cfg.StreamIdleTimeout). + DelayedCloseTimeout(cfg.DelayedCloseTimeout). + MaxConnectionDuration(cfg.MaxConnectionDuration). + ConnectionShutdownGracePeriod(cfg.ConnectionShutdownGracePeriod). + AllowChunkedLength(cfg.AllowChunkedLength). + AddFilter(envoy_v3.OriginalIPDetectionFilter(cfg.XffNumTrustedHops)). + AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). + Get() + + filters = envoy_v3.Filters(cm) + + alpnProtos = envoy_v3.ProtoNamesForVersions(cfg.DefaultHTTPVersions...) + } else { + filters = envoy_v3.Filters( + envoy_v3.TCPProxy(vh.ListenerName, + vh.TCPProxy, + cfg.newSecureAccessLog()), + ) - lv.visit(root) - - if httpListener, ok := lvc.HTTPListeners[lv.httpListenerName]; ok { - - // Add a listener if there are vhosts bound to http. - cm := envoy_v3.HTTPConnectionManagerBuilder(). - Codec(envoy_v3.CodecForVersions(lv.DefaultHTTPVersions...)). - DefaultFilters(). - RouteConfigName(httpListener.Name). - MetricsPrefix(httpListener.Name). - AccessLoggers(lvc.newInsecureAccessLog()). - RequestTimeout(lvc.RequestTimeout). - ConnectionIdleTimeout(lvc.ConnectionIdleTimeout). - StreamIdleTimeout(lvc.StreamIdleTimeout). - DelayedCloseTimeout(lvc.DelayedCloseTimeout). - MaxConnectionDuration(lvc.MaxConnectionDuration). - ConnectionShutdownGracePeriod(lvc.ConnectionShutdownGracePeriod). - AllowChunkedLength(lvc.AllowChunkedLength). - AddFilter(envoy_v3.OriginalIPDetectionFilter(lvc.XffNumTrustedHops)). - AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(lv.RateLimitConfig))). - Get() - - lv.listeners[httpListener.Name] = envoy_v3.Listener( - httpListener.Name, - httpListener.Address, - httpListener.Port, - proxyProtocol(lvc.UseProxyProto), - cm, - ) + // Do not offer ALPN for TCP proxying, since + // the protocols will be provided by the TCP + // backend in its ServerHello. + } + + var downstreamTLS *envoy_tls_v3.DownstreamTlsContext + + // Secret is provided when TLS is terminated and nil when TLS passthrough is used. + if vh.Secret != nil { + // Choose the higher of the configured or requested TLS version. + vers := max(cfg.minTLSVersion(), envoy_v3.ParseTLSVersion(vh.MinTLSVersion)) + + downstreamTLS = envoy_v3.DownstreamTLSContext( + vh.Secret, + vers, + cfg.CipherSuites, + vh.DownstreamValidation, + alpnProtos...) + } + + listeners[vh.ListenerName].FilterChains = append(listeners[vh.ListenerName].FilterChains, envoy_v3.FilterChainTLS(vh.VirtualHost.Name, downstreamTLS, filters)) + + // If this VirtualHost has enabled the fallback certificate then set a default + // FilterChain which will allow routes with this vhost to accept non-SNI TLS requests. + // Note that we don't add the misdirected requests filter on this chain because at this + // point we don't actually know the full set of server names that will be bound to the + // filter chain through the ENVOY_FALLBACK_ROUTECONFIG route configuration. + if vh.FallbackCertificate != nil && !envoy_v3.ContainsFallbackFilterChain(listeners[vh.ListenerName].FilterChains) { + // Construct the downstreamTLSContext passing the configured fallbackCertificate. The TLS minProtocolVersion will use + // the value defined in the Contour Configuration file if defined. + downstreamTLS = envoy_v3.DownstreamTLSContext( + vh.FallbackCertificate, + cfg.minTLSVersion(), + cfg.CipherSuites, + vh.DownstreamValidation, + alpnProtos..., + ) + + cm := envoy_v3.HTTPConnectionManagerBuilder(). + DefaultFilters(). + RouteConfigName(ENVOY_FALLBACK_ROUTECONFIG). + MetricsPrefix(vh.ListenerName). + AccessLoggers(cfg.newSecureAccessLog()). + RequestTimeout(cfg.RequestTimeout). + ConnectionIdleTimeout(cfg.ConnectionIdleTimeout). + StreamIdleTimeout(cfg.StreamIdleTimeout). + DelayedCloseTimeout(cfg.DelayedCloseTimeout). + MaxConnectionDuration(cfg.MaxConnectionDuration). + ConnectionShutdownGracePeriod(cfg.ConnectionShutdownGracePeriod). + AllowChunkedLength(cfg.AllowChunkedLength). + AddFilter(envoy_v3.OriginalIPDetectionFilter(cfg.XffNumTrustedHops)). + AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). + Get() + + // Default filter chain + filters = envoy_v3.Filters(cm) + + listeners[vh.ListenerName].FilterChains = append(listeners[vh.ListenerName].FilterChains, envoy_v3.FilterChainTLSFallback(downstreamTLS, filters)) + } + } } // Remove the https listener if there are no vhosts bound to it. - if len(lv.listeners[ENVOY_HTTPS_LISTENER].FilterChains) == 0 { - delete(lv.listeners, ENVOY_HTTPS_LISTENER) + if len(listeners[ENVOY_HTTPS_LISTENER].FilterChains) == 0 { + delete(listeners, ENVOY_HTTPS_LISTENER) } else { // there's some https listeners, we need to sort the filter chains // to ensure that the LDS entries are identical. - sort.Stable(sorter.For(lv.listeners[ENVOY_HTTPS_LISTENER].FilterChains)) + sort.Stable(sorter.For(listeners[ENVOY_HTTPS_LISTENER].FilterChains)) } // support more params of envoy listener // 1. connection balancer - if lvc.ConnectionBalancer == "exact" { - for _, listener := range lv.listeners { + if cfg.ConnectionBalancer == "exact" { + for _, listener := range listeners { listener.ConnectionBalanceConfig = &envoy_listener_v3.Listener_ConnectionBalanceConfig{ BalanceType: &envoy_listener_v3.Listener_ConnectionBalanceConfig_ExactBalance_{ ExactBalance: &envoy_listener_v3.Listener_ConnectionBalanceConfig_ExactBalance{}, @@ -546,7 +654,7 @@ func visitListeners(root dag.Vertex, lvc *ListenerConfig) map[string]*envoy_list } } - return lv.listeners + c.Update(listeners) } func envoyGlobalRateLimitConfig(config *RateLimitConfig) *envoy_v3.GlobalRateLimitConfig { @@ -575,135 +683,3 @@ func proxyProtocol(useProxy bool) []*envoy_listener_v3.ListenerFilter { func secureProxyProtocol(useProxy bool) []*envoy_listener_v3.ListenerFilter { return append(proxyProtocol(useProxy), envoy_v3.TLSInspector()) } - -func (v *listenerVisitor) visit(vertex dag.Vertex) { - max := func(a, b envoy_tls_v3.TlsParameters_TlsProtocol) envoy_tls_v3.TlsParameters_TlsProtocol { - if a > b { - return a - } - return b - } - - switch vh := vertex.(type) { - case *dag.VirtualHost: - // we only create on http listener so record the fact - // that we need to then double back at the end and add - // the listener properly - v.httpListenerName = vh.ListenerName - case *dag.SecureVirtualHost: - var alpnProtos []string - var filters []*envoy_listener_v3.Filter - - if vh.TCPProxy == nil { - var authFilter *http.HttpFilter - - if vh.AuthorizationService != nil { - authFilter = envoy_v3.FilterExternalAuthz( - vh.AuthorizationService.Name, - vh.AuthorizationFailOpen, - vh.AuthorizationResponseTimeout, - ) - } - - // Create a uniquely named HTTP connection manager for - // this vhost, so that the SNI name the client requests - // only grants access to that host. See RFC 6066 for - // security advice. Note that we still use the generic - // metrics prefix to keep compatibility with previous - // Contour versions since the metrics prefix will be - // coded into monitoring dashboards. - cm := envoy_v3.HTTPConnectionManagerBuilder(). - Codec(envoy_v3.CodecForVersions(v.DefaultHTTPVersions...)). - AddFilter(envoy_v3.FilterMisdirectedRequests(vh.VirtualHost.Name)). - DefaultFilters(). - AddFilter(authFilter). - RouteConfigName(path.Join("https", vh.VirtualHost.Name)). - MetricsPrefix(vh.ListenerName). - AccessLoggers(v.ListenerConfig.newSecureAccessLog()). - RequestTimeout(v.ListenerConfig.RequestTimeout). - ConnectionIdleTimeout(v.ListenerConfig.ConnectionIdleTimeout). - StreamIdleTimeout(v.ListenerConfig.StreamIdleTimeout). - DelayedCloseTimeout(v.ListenerConfig.DelayedCloseTimeout). - MaxConnectionDuration(v.ListenerConfig.MaxConnectionDuration). - ConnectionShutdownGracePeriod(v.ListenerConfig.ConnectionShutdownGracePeriod). - AllowChunkedLength(v.ListenerConfig.AllowChunkedLength). - AddFilter(envoy_v3.OriginalIPDetectionFilter(v.ListenerConfig.XffNumTrustedHops)). - AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(v.RateLimitConfig))). - Get() - - filters = envoy_v3.Filters(cm) - - alpnProtos = envoy_v3.ProtoNamesForVersions(v.DefaultHTTPVersions...) - } else { - filters = envoy_v3.Filters( - envoy_v3.TCPProxy(vh.ListenerName, - vh.TCPProxy, - v.ListenerConfig.newSecureAccessLog()), - ) - - // Do not offer ALPN for TCP proxying, since - // the protocols will be provided by the TCP - // backend in its ServerHello. - } - - var downstreamTLS *envoy_tls_v3.DownstreamTlsContext - - // Secret is provided when TLS is terminated and nil when TLS passthrough is used. - if vh.Secret != nil { - // Choose the higher of the configured or requested TLS version. - vers := max(v.ListenerConfig.minTLSVersion(), envoy_v3.ParseTLSVersion(vh.MinTLSVersion)) - - downstreamTLS = envoy_v3.DownstreamTLSContext( - vh.Secret, - vers, - v.ListenerConfig.CipherSuites, - vh.DownstreamValidation, - alpnProtos...) - } - - v.listeners[vh.ListenerName].FilterChains = append(v.listeners[vh.ListenerName].FilterChains, - envoy_v3.FilterChainTLS(vh.VirtualHost.Name, downstreamTLS, filters)) - - // If this VirtualHost has enabled the fallback certificate then set a default - // FilterChain which will allow routes with this vhost to accept non-SNI TLS requests. - // Note that we don't add the misdirected requests filter on this chain because at this - // point we don't actually know the full set of server names that will be bound to the - // filter chain through the ENVOY_FALLBACK_ROUTECONFIG route configuration. - if vh.FallbackCertificate != nil && !envoy_v3.ContainsFallbackFilterChain(v.listeners[vh.ListenerName].FilterChains) { - // Construct the downstreamTLSContext passing the configured fallbackCertificate. The TLS minProtocolVersion will use - // the value defined in the Contour Configuration file if defined. - downstreamTLS = envoy_v3.DownstreamTLSContext( - vh.FallbackCertificate, - v.ListenerConfig.minTLSVersion(), - v.ListenerConfig.CipherSuites, - vh.DownstreamValidation, - alpnProtos...) - - cm := envoy_v3.HTTPConnectionManagerBuilder(). - DefaultFilters(). - RouteConfigName(ENVOY_FALLBACK_ROUTECONFIG). - MetricsPrefix(vh.ListenerName). - AccessLoggers(v.ListenerConfig.newSecureAccessLog()). - RequestTimeout(v.ListenerConfig.RequestTimeout). - ConnectionIdleTimeout(v.ListenerConfig.ConnectionIdleTimeout). - StreamIdleTimeout(v.ListenerConfig.StreamIdleTimeout). - DelayedCloseTimeout(v.ListenerConfig.DelayedCloseTimeout). - MaxConnectionDuration(v.ListenerConfig.MaxConnectionDuration). - ConnectionShutdownGracePeriod(v.ListenerConfig.ConnectionShutdownGracePeriod). - AllowChunkedLength(v.ListenerConfig.AllowChunkedLength). - AddFilter(envoy_v3.OriginalIPDetectionFilter(v.ListenerConfig.XffNumTrustedHops)). - AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(v.RateLimitConfig))). - Get() - - // Default filter chain - filters = envoy_v3.Filters(cm) - - v.listeners[vh.ListenerName].FilterChains = append(v.listeners[vh.ListenerName].FilterChains, - envoy_v3.FilterChainTLSFallback(downstreamTLS, filters)) - } - - default: - // recurse - vertex.Visit(v.visit) - } -} diff --git a/internal/xdscache/v3/listener_test.go b/internal/xdscache/v3/listener_test.go index 935f5911897..715f42a6382 100644 --- a/internal/xdscache/v3/listener_test.go +++ b/internal/xdscache/v3/listener_test.go @@ -2144,7 +2144,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with connection idle timeout set in visitor config": { + "httpproxy with connection idle timeout set in listener config": { ListenerConfig: ListenerConfig{ ConnectionIdleTimeout: timeout.DurationSetting(90 * time.Second), }, @@ -2198,7 +2198,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with stream idle timeout set in visitor config": { + "httpproxy with stream idle timeout set in listener config": { ListenerConfig: ListenerConfig{ StreamIdleTimeout: timeout.DurationSetting(90 * time.Second), }, @@ -2252,7 +2252,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with max connection duration set in visitor config": { + "httpproxy with max connection duration set in listener config": { ListenerConfig: ListenerConfig{ MaxConnectionDuration: timeout.DurationSetting(90 * time.Second), }, @@ -2306,7 +2306,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with delayed close timeout set in visitor config": { + "httpproxy with delayed close timeout set in listener config": { ListenerConfig: ListenerConfig{ DelayedCloseTimeout: timeout.DurationSetting(90 * time.Second), }, @@ -2360,7 +2360,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with connection shutdown grace period set in visitor config": { + "httpproxy with connection shutdown grace period set in listener config": { ListenerConfig: ListenerConfig{ ConnectionShutdownGracePeriod: timeout.DurationSetting(90 * time.Second), }, @@ -2414,7 +2414,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpsproxy with secret with connection idle timeout set in visitor config": { + "httpsproxy with secret with connection idle timeout set in listener config": { ListenerConfig: ListenerConfig{ ConnectionIdleTimeout: timeout.DurationSetting(90 * time.Second), }, @@ -2496,7 +2496,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with allow_chunked_length set in visitor config": { + "httpproxy with allow_chunked_length set in listener config": { ListenerConfig: ListenerConfig{ AllowChunkedLength: true, }, @@ -2550,7 +2550,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpproxy with XffNumTrustedHops set in visitor config": { + "httpproxy with XffNumTrustedHops set in listener config": { ListenerConfig: ListenerConfig{ XffNumTrustedHops: 1, }, @@ -2604,7 +2604,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpsproxy with secret with stream idle timeout set in visitor config": { + "httpsproxy with secret with stream idle timeout set in listener config": { ListenerConfig: ListenerConfig{ StreamIdleTimeout: timeout.DurationSetting(90 * time.Second), }, @@ -2686,7 +2686,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpsproxy with secret with max connection duration set in visitor config": { + "httpsproxy with secret with max connection duration set in listener config": { ListenerConfig: ListenerConfig{ MaxConnectionDuration: timeout.DurationSetting(90 * time.Second), }, @@ -2768,7 +2768,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpsproxy with secret with delayed close timeout set in visitor config": { + "httpsproxy with secret with delayed close timeout set in listener config": { ListenerConfig: ListenerConfig{ DelayedCloseTimeout: timeout.DurationSetting(90 * time.Second), }, @@ -2850,7 +2850,7 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, - "httpsproxy with secret with connection shutdown grace period set in visitor config": { + "httpsproxy with secret with connection shutdown grace period set in listener config": { ListenerConfig: ListenerConfig{ ConnectionShutdownGracePeriod: timeout.DurationSetting(90 * time.Second), }, @@ -3321,9 +3321,11 @@ func TestListenerVisit(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - root := buildDAGFallback(t, tc.fallbackCertificate, tc.objs...) - got := visitListeners(root, &tc.ListenerConfig) - protobuf.ExpectEqual(t, tc.want, got) + lc := ListenerCache{ + Config: tc.ListenerConfig, + } + lc.OnChange(buildDAGFallback(t, tc.fallbackCertificate, tc.objs...)) + protobuf.ExpectEqual(t, tc.want, lc.values) }) } } diff --git a/internal/xdscache/v3/route.go b/internal/xdscache/v3/route.go index f5f0d701b12..5c40a22e17d 100644 --- a/internal/xdscache/v3/route.go +++ b/internal/xdscache/v3/route.go @@ -88,207 +88,146 @@ func (c *RouteCache) Query(names []string) []proto.Message { func (*RouteCache) TypeURL() string { return resource.RouteType } func (c *RouteCache) OnChange(root *dag.DAG) { - routes := visitRoutes(root) - c.Update(routes) -} - -type routeVisitor struct { - routes map[string]*envoy_route_v3.RouteConfiguration -} - -func visitRoutes(root dag.Vertex) map[string]*envoy_route_v3.RouteConfiguration { - // Collect the route configurations for all the routes we can - // find. For HTTP hosts, the routes will all be collected on the - // well-known ENVOY_HTTP_LISTENER, but for HTTPS hosts, we will - // generate a per-vhost collection. This lets us keep different - // SNI names disjoint when we later configure the listener. - rv := routeVisitor{ - routes: map[string]*envoy_route_v3.RouteConfiguration{ - ENVOY_HTTP_LISTENER: envoy_v3.RouteConfiguration(ENVOY_HTTP_LISTENER), - }, + // RouteConfigs keyed by RouteConfig name: + // - one for all the HTTP vhost routes -- "ingress_http" + // - one per svhost -- "https/" + // - one for fallback cert (if configured) -- "ingress_fallbackcert" + routeConfigs := map[string]*envoy_route_v3.RouteConfiguration{ + ENVOY_HTTP_LISTENER: envoy_v3.RouteConfiguration(ENVOY_HTTP_LISTENER), } - rv.visit(root) - - for _, v := range rv.routes { - sort.Stable(sorter.For(v.VirtualHosts)) - } - - return rv.routes -} - -func (v *routeVisitor) onVirtualHost(vh *dag.VirtualHost) { - var routes []*dag.Route - - vh.Visit(func(v dag.Vertex) { - route, ok := v.(*dag.Route) - if !ok { - return - } - routes = append(routes, route) - }) - - if len(routes) == 0 { - return - } - - toEnvoyRoute := func(route *dag.Route) *envoy_route_v3.Route { - switch { - case route.HTTPSUpgrade: - // TODO(dfc) if we ensure the builder never returns a dag.Route connected - // to a SecureVirtualHost that requires upgrade, this logic can move to - // envoy.RouteRoute. - return &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.UpgradeHTTPS(), - } - case route.DirectResponse != nil: - return &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.RouteDirectResponse(route.DirectResponse), - } - case route.Redirect != nil: - // TODO request/response headers? - return &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.RouteRedirect(route.Redirect), - } - default: - rt := &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.RouteRoute(route), - } - if route.RequestHeadersPolicy != nil { - rt.RequestHeadersToAdd = append(envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Set, false), envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Add, true)...) - rt.RequestHeadersToRemove = route.RequestHeadersPolicy.Remove - } - if route.ResponseHeadersPolicy != nil { - rt.ResponseHeadersToAdd = envoy_v3.HeaderValueList(route.ResponseHeadersPolicy.Set, false) - rt.ResponseHeadersToRemove = route.ResponseHeadersPolicy.Remove - } - if route.RateLimitPolicy != nil && route.RateLimitPolicy.Local != nil { - if rt.TypedPerFilterConfig == nil { - rt.TypedPerFilterConfig = map[string]*any.Any{} + for vhost, routes := range root.GetVirtualHostRoutes() { + toEnvoyRoute := func(route *dag.Route) *envoy_route_v3.Route { + switch { + case route.HTTPSUpgrade: + // TODO(dfc) if we ensure the builder never returns a dag.Route connected + // to a SecureVirtualHost that requires upgrade, this logic can move to + // envoy.RouteRoute. + return &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.UpgradeHTTPS(), + } + case route.DirectResponse != nil: + return &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.RouteDirectResponse(route.DirectResponse), + } + case route.Redirect != nil: + // TODO request/response headers? + return &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.RouteRedirect(route.Redirect), + } + default: + rt := &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.RouteRoute(route), + } + if route.RequestHeadersPolicy != nil { + rt.RequestHeadersToAdd = append(envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Set, false), envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Add, true)...) + rt.RequestHeadersToRemove = route.RequestHeadersPolicy.Remove + } + if route.ResponseHeadersPolicy != nil { + rt.ResponseHeadersToAdd = envoy_v3.HeaderValueList(route.ResponseHeadersPolicy.Set, false) + rt.ResponseHeadersToRemove = route.ResponseHeadersPolicy.Remove + } + if route.RateLimitPolicy != nil && route.RateLimitPolicy.Local != nil { + if rt.TypedPerFilterConfig == nil { + rt.TypedPerFilterConfig = map[string]*any.Any{} + } + rt.TypedPerFilterConfig["envoy.filters.http.local_ratelimit"] = envoy_v3.LocalRateLimitConfig(route.RateLimitPolicy.Local, "vhost."+vhost.Name) } - rt.TypedPerFilterConfig["envoy.filters.http.local_ratelimit"] = envoy_v3.LocalRateLimitConfig(route.RateLimitPolicy.Local, "vhost."+vh.Name) - } - return rt - } - } - - sortRoutes(routes) - v.routes[ENVOY_HTTP_LISTENER].VirtualHosts = append(v.routes[ENVOY_HTTP_LISTENER].VirtualHosts, toEnvoyVirtualHost(vh, routes, toEnvoyRoute)) -} - -func (v *routeVisitor) onSecureVirtualHost(svh *dag.SecureVirtualHost) { - var routes []*dag.Route - svh.Visit(func(v dag.Vertex) { - route, ok := v.(*dag.Route) - if !ok { - return + return rt + } } - routes = append(routes, route) - }) - if len(routes) == 0 { - return + sortRoutes(routes) + routeConfigs[ENVOY_HTTP_LISTENER].VirtualHosts = append(routeConfigs[ENVOY_HTTP_LISTENER].VirtualHosts, toEnvoyVirtualHost(vhost, routes, toEnvoyRoute)) } - toEnvoyRoute := func(route *dag.Route) *envoy_route_v3.Route { - switch { - case route.DirectResponse != nil: - return &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.RouteDirectResponse(route.DirectResponse), - } - case route.Redirect != nil: - // TODO request/response headers? - return &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.RouteRedirect(route.Redirect), - } - default: - rt := &envoy_route_v3.Route{ - Match: envoy_v3.RouteMatch(route), - Action: envoy_v3.RouteRoute(route), - } - - if route.RequestHeadersPolicy != nil { - rt.RequestHeadersToAdd = append(envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Set, false), envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Add, true)...) - rt.RequestHeadersToRemove = route.RequestHeadersPolicy.Remove - } - if route.ResponseHeadersPolicy != nil { - rt.ResponseHeadersToAdd = envoy_v3.HeaderValueList(route.ResponseHeadersPolicy.Set, false) - rt.ResponseHeadersToRemove = route.ResponseHeadersPolicy.Remove - } - if route.RateLimitPolicy != nil && route.RateLimitPolicy.Local != nil { - if rt.TypedPerFilterConfig == nil { - rt.TypedPerFilterConfig = map[string]*any.Any{} + for vhost, routes := range root.GetSecureVirtualHostRoutes() { + toEnvoyRoute := func(route *dag.Route) *envoy_route_v3.Route { + switch { + case route.DirectResponse != nil: + return &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.RouteDirectResponse(route.DirectResponse), + } + case route.Redirect != nil: + // TODO request/response headers? + return &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.RouteRedirect(route.Redirect), + } + default: + rt := &envoy_route_v3.Route{ + Match: envoy_v3.RouteMatch(route), + Action: envoy_v3.RouteRoute(route), } - rt.TypedPerFilterConfig["envoy.filters.http.local_ratelimit"] = envoy_v3.LocalRateLimitConfig(route.RateLimitPolicy.Local, "vhost."+svh.Name) - } - // If authorization is enabled on this host, we may need to set per-route filter overrides. - if svh.AuthorizationService != nil { - // Apply per-route authorization policy modifications. - if route.AuthDisabled { + if route.RequestHeadersPolicy != nil { + rt.RequestHeadersToAdd = append(envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Set, false), envoy_v3.HeaderValueList(route.RequestHeadersPolicy.Add, true)...) + rt.RequestHeadersToRemove = route.RequestHeadersPolicy.Remove + } + if route.ResponseHeadersPolicy != nil { + rt.ResponseHeadersToAdd = envoy_v3.HeaderValueList(route.ResponseHeadersPolicy.Set, false) + rt.ResponseHeadersToRemove = route.ResponseHeadersPolicy.Remove + } + if route.RateLimitPolicy != nil && route.RateLimitPolicy.Local != nil { if rt.TypedPerFilterConfig == nil { rt.TypedPerFilterConfig = map[string]*any.Any{} } - rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = envoy_v3.RouteAuthzDisabled() - } else if len(route.AuthContext) > 0 { - if rt.TypedPerFilterConfig == nil { - rt.TypedPerFilterConfig = map[string]*any.Any{} + rt.TypedPerFilterConfig["envoy.filters.http.local_ratelimit"] = envoy_v3.LocalRateLimitConfig(route.RateLimitPolicy.Local, "vhost."+vhost.Name) + } + + // If authorization is enabled on this host, we may need to set per-route filter overrides. + if vhost.AuthorizationService != nil { + // Apply per-route authorization policy modifications. + if route.AuthDisabled { + if rt.TypedPerFilterConfig == nil { + rt.TypedPerFilterConfig = map[string]*any.Any{} + } + rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = envoy_v3.RouteAuthzDisabled() + } else if len(route.AuthContext) > 0 { + if rt.TypedPerFilterConfig == nil { + rt.TypedPerFilterConfig = map[string]*any.Any{} + } + rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = envoy_v3.RouteAuthzContext(route.AuthContext) } - rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = envoy_v3.RouteAuthzContext(route.AuthContext) } + + return rt } + } - return rt + // Add secure vhost route config if not already present. + name := path.Join("https", vhost.VirtualHost.Name) + if _, ok := routeConfigs[name]; !ok { + routeConfigs[name] = envoy_v3.RouteConfiguration(name) } - } - // Add secure vhost route config if not already present. - name := path.Join("https", svh.VirtualHost.Name) - if _, ok := v.routes[name]; !ok { - v.routes[name] = envoy_v3.RouteConfiguration(name) - } + sortRoutes(routes) + routeConfigs[name].VirtualHosts = append(routeConfigs[name].VirtualHosts, toEnvoyVirtualHost(&vhost.VirtualHost, routes, toEnvoyRoute)) - sortRoutes(routes) - v.routes[name].VirtualHosts = append(v.routes[name].VirtualHosts, toEnvoyVirtualHost(&svh.VirtualHost, routes, toEnvoyRoute)) + // A fallback route configuration contains routes for all the vhosts that have the fallback certificate enabled. + // When a request is received, the default TLS filterchain will accept the connection, + // and this routing table in RDS defines where the request proxies next. + if vhost.FallbackCertificate != nil { + // Add fallback route config if not already present. + if _, ok := routeConfigs[ENVOY_FALLBACK_ROUTECONFIG]; !ok { + routeConfigs[ENVOY_FALLBACK_ROUTECONFIG] = envoy_v3.RouteConfiguration(ENVOY_FALLBACK_ROUTECONFIG) + } - // A fallback route configuration contains routes for all the vhosts that have the fallback certificate enabled. - // When a request is received, the default TLS filterchain will accept the connection, - // and this routing table in RDS defines where the request proxies next. - if svh.FallbackCertificate != nil { - // Add fallback route config if not already present. - if _, ok := v.routes[ENVOY_FALLBACK_ROUTECONFIG]; !ok { - v.routes[ENVOY_FALLBACK_ROUTECONFIG] = envoy_v3.RouteConfiguration(ENVOY_FALLBACK_ROUTECONFIG) + routeConfigs[ENVOY_FALLBACK_ROUTECONFIG].VirtualHosts = append(routeConfigs[ENVOY_FALLBACK_ROUTECONFIG].VirtualHosts, toEnvoyVirtualHost(&vhost.VirtualHost, routes, toEnvoyRoute)) } - - v.routes[ENVOY_FALLBACK_ROUTECONFIG].VirtualHosts = append(v.routes[ENVOY_FALLBACK_ROUTECONFIG].VirtualHosts, toEnvoyVirtualHost(&svh.VirtualHost, routes, toEnvoyRoute)) } -} -func (v *routeVisitor) visit(vertex dag.Vertex) { - switch l := vertex.(type) { - case *dag.Listener: - l.Visit(func(vertex dag.Vertex) { - switch vh := vertex.(type) { - case *dag.VirtualHost: - v.onVirtualHost(vh) - case *dag.SecureVirtualHost: - v.onSecureVirtualHost(vh) - default: - // recurse - vertex.Visit(v.visit) - } - }) - default: - // recurse - vertex.Visit(v.visit) + for _, routeConfig := range routeConfigs { + sort.Stable(sorter.For(routeConfig.VirtualHosts)) } + + c.Update(routeConfigs) } // sortRoutes sorts the given Route slice in place. Routes are ordered diff --git a/internal/xdscache/v3/route_test.go b/internal/xdscache/v3/route_test.go index b83c1a3fbaf..e7468b38a26 100644 --- a/internal/xdscache/v3/route_test.go +++ b/internal/xdscache/v3/route_test.go @@ -3057,9 +3057,9 @@ func TestRouteVisit(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - root := buildDAGFallback(t, tc.fallbackCertificate, tc.objs...) - got := visitRoutes(root) - protobuf.ExpectEqual(t, tc.want, got) + var rc RouteCache + rc.OnChange(buildDAGFallback(t, tc.fallbackCertificate, tc.objs...)) + protobuf.ExpectEqual(t, tc.want, rc.values) }) } } diff --git a/internal/xdscache/v3/secret.go b/internal/xdscache/v3/secret.go index 811f1f5f154..017db3487d7 100644 --- a/internal/xdscache/v3/secret.go +++ b/internal/xdscache/v3/secret.go @@ -75,45 +75,14 @@ func (c *SecretCache) Query(names []string) []proto.Message { func (*SecretCache) TypeURL() string { return resource.SecretType } func (c *SecretCache) OnChange(root *dag.DAG) { - secrets := visitSecrets(root) - c.Update(secrets) -} - -type secretVisitor struct { - secrets map[string]*envoy_tls_v3.Secret -} - -// visitSecrets produces a map of *envoy_tls_v3.Secret -func visitSecrets(root dag.Vertex) map[string]*envoy_tls_v3.Secret { - sv := secretVisitor{ - secrets: make(map[string]*envoy_tls_v3.Secret), - } - sv.visit(root) - return sv.secrets -} + secrets := map[string]*envoy_tls_v3.Secret{} -func (v *secretVisitor) addSecret(s *dag.Secret) { - name := envoy.Secretname(s) - if _, ok := v.secrets[name]; !ok { - envoySecret := envoy_v3.Secret(s) - v.secrets[envoySecret.Name] = envoySecret - } -} - -func (v *secretVisitor) visit(vertex dag.Vertex) { - switch obj := vertex.(type) { - case *dag.SecureVirtualHost: - if obj.Secret != nil { - v.addSecret(obj.Secret) - } - if obj.FallbackCertificate != nil { - v.addSecret(obj.FallbackCertificate) + for _, secret := range root.GetSecrets() { + name := envoy.Secretname(secret) + if _, ok := secrets[name]; !ok { + secrets[name] = envoy_v3.Secret(secret) } - case *dag.Cluster: - if obj.ClientCertificate != nil { - v.addSecret(obj.ClientCertificate) - } - default: - vertex.Visit(v.visit) } + + c.Update(secrets) } diff --git a/internal/xdscache/v3/secret_test.go b/internal/xdscache/v3/secret_test.go index 8a0fb58ecbd..b95c56723e1 100644 --- a/internal/xdscache/v3/secret_test.go +++ b/internal/xdscache/v3/secret_test.go @@ -469,9 +469,9 @@ func TestSecretVisit(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - root := buildDAG(t, tc.objs...) - got := visitSecrets(root) - protobuf.ExpectEqual(t, tc.want, got) + var sc SecretCache + sc.OnChange(buildDAG(t, tc.objs...)) + protobuf.ExpectEqual(t, tc.want, sc.values) }) } } diff --git a/internal/xdscache/v3/visitor_test.go b/internal/xdscache/v3/visitor_test.go deleted file mode 100644 index 454a1d99220..00000000000 --- a/internal/xdscache/v3/visitor_test.go +++ /dev/null @@ -1,230 +0,0 @@ -// 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 ( - "testing" - - envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" - envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - "github.com/projectcontour/contour/internal/dag" - envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" - "github.com/projectcontour/contour/internal/protobuf" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func TestVisitClusters(t *testing.T) { - tests := map[string]struct { - root dag.Vertex - want map[string]*envoy_cluster_v3.Cluster - }{ - "TCPService forward": { - root: &dag.Listener{ - Port: 443, - VirtualHosts: virtualhosts( - &dag.SecureVirtualHost{ - VirtualHost: dag.VirtualHost{ - Name: "www.example.com", - }, - TCPProxy: &dag.TCPProxy{ - Clusters: []*dag.Cluster{{ - Upstream: &dag.Service{ - Weighted: dag.WeightedService{ - Weight: 1, - ServiceName: "example", - ServiceNamespace: "default", - ServicePort: v1.ServicePort{ - Protocol: "TCP", - Port: 443, - TargetPort: intstr.FromInt(8443), - }, - }, - }, - }}, - }, - Secret: new(dag.Secret), - }, - ), - }, - want: clustermap( - &envoy_cluster_v3.Cluster{ - Name: "default/example/443/da39a3ee5e", - AltStatName: "default_example_443", - ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), - EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ - EdsConfig: envoy_v3.ConfigSource("contour"), - ServiceName: "default/example", - }, - }, - ), - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - got := visitClusters(tc.root) - protobuf.ExpectEqual(t, tc.want, got) - }) - } -} - -func TestVisitListeners(t *testing.T) { - p1 := &dag.TCPProxy{ - Clusters: []*dag.Cluster{{ - Upstream: &dag.Service{ - Weighted: dag.WeightedService{ - Weight: 1, - ServiceName: "example", - ServiceNamespace: "default", - ServicePort: v1.ServicePort{ - Protocol: "TCP", - Port: 443, - TargetPort: intstr.FromInt(8443), - }, - }, - }, - }}, - } - - tests := map[string]struct { - root dag.Vertex - want map[string]*envoy_listener_v3.Listener - }{ - "TCPService forward": { - root: &dag.Listener{ - Port: 443, - VirtualHosts: virtualhosts( - &dag.SecureVirtualHost{ - VirtualHost: dag.VirtualHost{ - Name: "tcpproxy.example.com", - ListenerName: "ingress_https", - }, - TCPProxy: p1, - Secret: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: secretdata(CERTIFICATE, RSA_PRIVATE_KEY), - }, - }, - MinTLSVersion: "1.2", - }, - ), - }, - want: listenermap( - &envoy_listener_v3.Listener{ - Name: ENVOY_HTTPS_LISTENER, - Address: envoy_v3.SocketAddress("0.0.0.0", 8443), - FilterChains: []*envoy_listener_v3.FilterChain{{ - FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ - ServerNames: []string{"tcpproxy.example.com"}, - }, - TransportSocket: transportSocket("secret", envoy_tls_v3.TlsParameters_TLSv1_2, nil), - Filters: envoy_v3.Filters(envoy_v3.TCPProxy(ENVOY_HTTPS_LISTENER, p1, envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTPS_ACCESS_LOG, "", nil))), - }}, - ListenerFilters: envoy_v3.ListenerFilters( - envoy_v3.TLSInspector(), - ), - SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), - }, - ), - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - got := visitListeners(tc.root, new(ListenerConfig)) - protobuf.ExpectEqual(t, tc.want, got) - }) - } -} - -func TestVisitSecrets(t *testing.T) { - tests := map[string]struct { - root dag.Vertex - want map[string]*envoy_tls_v3.Secret - }{ - "TCPService forward": { - root: &dag.Listener{ - Port: 443, - VirtualHosts: virtualhosts( - &dag.SecureVirtualHost{ - VirtualHost: dag.VirtualHost{ - Name: "www.example.com", - }, - TCPProxy: &dag.TCPProxy{ - Clusters: []*dag.Cluster{{ - Upstream: &dag.Service{ - Weighted: dag.WeightedService{ - Weight: 1, - ServiceName: "example", - ServiceNamespace: "default", - ServicePort: v1.ServicePort{ - Protocol: "TCP", - Port: 443, - TargetPort: intstr.FromInt(8443), - }, - }, - }, - }}, - }, - Secret: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: secretdata("certificate", "key"), - }, - }, - }, - ), - }, - want: secretmap(&envoy_tls_v3.Secret{ - Name: "default/secret/735ad571c1", - Type: &envoy_tls_v3.Secret_TlsCertificate{ - TlsCertificate: &envoy_tls_v3.TlsCertificate{ - PrivateKey: &envoy_core_v3.DataSource{ - Specifier: &envoy_core_v3.DataSource_InlineBytes{ - InlineBytes: []byte("key"), - }, - }, - CertificateChain: &envoy_core_v3.DataSource{ - Specifier: &envoy_core_v3.DataSource_InlineBytes{ - InlineBytes: []byte("certificate"), - }, - }, - }, - }, - }), - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - got := visitSecrets(tc.root) - protobuf.ExpectEqual(t, tc.want, got) - }) - } -} - -func virtualhosts(vx ...dag.Vertex) []dag.Vertex { - return vx -}