diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index e56679492a7..cc187bcba42 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -1762,6 +1762,106 @@ func TestDAGInsert(t *testing.T) { }, } + // proxy2d is a proxy with two routes that have the same prefix and a Contains header + // condition on the same header, differing only in the value of the condition. + proxy2d := &projcontour.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com", + Namespace: "default", + }, + Spec: projcontour.HTTPProxySpec{ + VirtualHost: &projcontour.VirtualHost{ + Fqdn: "example.com", + }, + Routes: []projcontour.Route{ + { + Conditions: []projcontour.Condition{ + { + Header: &projcontour.HeaderCondition{ + Name: "e-tag", + Contains: "abc", + }, + }, + { + Prefix: "/", + }, + }, + Services: []projcontour.Service{{ + Name: "kuard", + Port: 8080, + }}, + }, + { + Conditions: []projcontour.Condition{ + { + Header: &projcontour.HeaderCondition{ + Name: "e-tag", + Contains: "def", + }, + }, + { + Prefix: "/", + }, + }, + Services: []projcontour.Service{{ + Name: "kuard", + Port: 8080, + }}, + }, + }, + }, + } + + // proxy2e is a proxy with two routes that both have a condition on the same + // header, one using Contains and one using NotContains. + proxy2e := &projcontour.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com", + Namespace: "default", + }, + Spec: projcontour.HTTPProxySpec{ + VirtualHost: &projcontour.VirtualHost{ + Fqdn: "example.com", + }, + Routes: []projcontour.Route{ + { + Conditions: []projcontour.Condition{ + { + Header: &projcontour.HeaderCondition{ + Name: "e-tag", + Contains: "abc", + }, + }, + { + Prefix: "/", + }, + }, + Services: []projcontour.Service{{ + Name: "kuard", + Port: 8080, + }}, + }, + { + Conditions: []projcontour.Condition{ + { + Header: &projcontour.HeaderCondition{ + Name: "e-tag", + NotContains: "abc", + }, + }, + { + Prefix: "/", + }, + }, + Services: []projcontour.Service{{ + Name: "kuard", + Port: 8080, + }}, + }, + }, + }, + } + // proxy6 has TLS and does not specify min tls version proxy6 := &projcontour.HTTPProxy{ ObjectMeta: metav1.ObjectMeta{ @@ -4231,6 +4331,56 @@ func TestDAGInsert(t *testing.T) { }, ), }, + "insert httproxy w/ multiple routes with a Contains condition on the same header": { + objs: []interface{}{ + proxy2d, s1, + }, + want: listeners( + &Listener{ + Port: 80, + VirtualHosts: virtualhosts( + virtualhost("example.com", &Route{ + PathCondition: &PrefixCondition{Prefix: "/"}, + HeaderConditions: []HeaderCondition{ + {Name: "e-tag", Value: "abc", MatchType: "contains"}, + }, + Clusters: clusters(service(s1)), + }, &Route{ + PathCondition: &PrefixCondition{Prefix: "/"}, + HeaderConditions: []HeaderCondition{ + {Name: "e-tag", Value: "def", MatchType: "contains"}, + }, + Clusters: clusters(service(s1)), + }), + ), + }, + ), + }, + "insert httproxy w/ multiple routes with condition on the same header, one Contains and one NotContains": { + objs: []interface{}{ + proxy2e, s1, + }, + want: listeners( + &Listener{ + Port: 80, + VirtualHosts: virtualhosts( + virtualhost("example.com", &Route{ + PathCondition: &PrefixCondition{Prefix: "/"}, + HeaderConditions: []HeaderCondition{ + {Name: "e-tag", Value: "abc", MatchType: "contains"}, + }, + Clusters: clusters(service(s1)), + }, &Route{ + PathCondition: &PrefixCondition{Prefix: "/"}, + HeaderConditions: []HeaderCondition{ + {Name: "e-tag", Value: "abc", MatchType: "contains", Invert: true}, + }, + Clusters: clusters(service(s1)), + }), + ), + }, + ), + }, "insert httproxy w/ included conditions": { objs: []interface{}{ proxy2a, proxy2b, s1, diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 1de2d76ca94..a6fa83b57eb 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -17,6 +17,7 @@ package dag import ( "fmt" + "strconv" "strings" "time" @@ -79,7 +80,14 @@ type HeaderCondition struct { } func (hc *HeaderCondition) String() string { - return "header: " + hc.Name + details := strings.Join([]string{ + "name=" + hc.Name, + "value=" + hc.Value, + "matchtype=", hc.MatchType, + "invert=", strconv.FormatBool(hc.Invert), + }, "&") + + return "header: " + details } // Route defines the properties of a route to a Cluster. diff --git a/internal/featuretests/headercondition_test.go b/internal/featuretests/headercondition_test.go index dfdc8f30582..366a974e976 100644 --- a/internal/featuretests/headercondition_test.go +++ b/internal/featuretests/headercondition_test.go @@ -388,4 +388,122 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { ), TypeUrl: routeType, }) + + // proxy with two routes that have the same prefix and a Contains header + // condition on the same header, differing only in the value of the condition. + proxy6 := fixture.NewProxy("simple").WithSpec( + projcontour.HTTPProxySpec{ + VirtualHost: &projcontour.VirtualHost{Fqdn: "hello.world"}, + Routes: []projcontour.Route{ + { + Conditions: conditions( + prefixCondition("/"), + headerContainsCondition("x-header", "abc"), + ), + Services: []projcontour.Service{{ + Name: "svc1", + Port: 80, + }}, + }, + { + Conditions: conditions( + prefixCondition("/"), + headerContainsCondition("x-header", "def"), + ), + Services: []projcontour.Service{{ + Name: "svc2", + Port: 80, + }}, + }}, + }, + ) + + rh.OnUpdate(proxy5, proxy6) + + c.Request(routeType).Equals(&v2.DiscoveryResponse{ + Resources: resources(t, + envoy.RouteConfiguration("ingress_http", + envoy.VirtualHost("hello.world", + &envoy_api_v2_route.Route{ + Match: routePrefix("/", dag.HeaderCondition{ + Name: "x-header", + Value: "abc", + MatchType: "contains", + Invert: false, + }), + Action: routeCluster("default/svc1/80/da39a3ee5e"), + }, + &envoy_api_v2_route.Route{ + Match: routePrefix("/", dag.HeaderCondition{ + Name: "x-header", + Value: "def", + MatchType: "contains", + Invert: false, + }), + Action: routeCluster("default/svc2/80/da39a3ee5e"), + }, + ), + ), + ), + TypeUrl: routeType, + }) + + // proxy with two routes that both have a condition on the same + // header, one using Contains and one using NotContains. + proxy7 := fixture.NewProxy("simple").WithSpec( + projcontour.HTTPProxySpec{ + VirtualHost: &projcontour.VirtualHost{Fqdn: "hello.world"}, + Routes: []projcontour.Route{ + { + Conditions: conditions( + prefixCondition("/"), + headerContainsCondition("x-header", "abc"), + ), + Services: []projcontour.Service{{ + Name: "svc1", + Port: 80, + }}, + }, + { + Conditions: conditions( + prefixCondition("/"), + headerNotContainsCondition("x-header", "abc"), + ), + Services: []projcontour.Service{{ + Name: "svc2", + Port: 80, + }}, + }}, + }, + ) + + rh.OnUpdate(proxy6, proxy7) + + c.Request(routeType).Equals(&v2.DiscoveryResponse{ + Resources: resources(t, + envoy.RouteConfiguration("ingress_http", + envoy.VirtualHost("hello.world", + &envoy_api_v2_route.Route{ + Match: routePrefix("/", dag.HeaderCondition{ + Name: "x-header", + Value: "abc", + MatchType: "contains", + Invert: false, + }), + Action: routeCluster("default/svc1/80/da39a3ee5e"), + }, + &envoy_api_v2_route.Route{ + Match: routePrefix("/", dag.HeaderCondition{ + Name: "x-header", + Value: "abc", + MatchType: "contains", + Invert: true, + }), + Action: routeCluster("default/svc2/80/da39a3ee5e"), + }, + ), + ), + ), + TypeUrl: routeType, + }) }