diff --git a/api/envoy/config/route/v3/BUILD b/api/envoy/config/route/v3/BUILD index 81cdfdf8a93a..b82843eee7dd 100644 --- a/api/envoy/config/route/v3/BUILD +++ b/api/envoy/config/route/v3/BUILD @@ -15,5 +15,7 @@ api_proto_package( "//envoy/type/tracing/v3:pkg", "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", + "@com_github_cncf_udpa//xds/annotations/v3:pkg", + "@com_github_cncf_udpa//xds/type/matcher/v3:pkg", ], ) diff --git a/api/envoy/config/route/v3/route_components.proto b/api/envoy/config/route/v3/route_components.proto index d25edd756db5..5a915eee87ca 100644 --- a/api/envoy/config/route/v3/route_components.proto +++ b/api/envoy/config/route/v3/route_components.proto @@ -17,6 +17,9 @@ import "google/protobuf/any.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; +import "xds/annotations/v3/status.proto"; +import "xds/type/matcher/v3/matcher.proto"; + import "envoy/annotations/deprecation.proto"; import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; @@ -37,7 +40,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // host header. This allows a single listener to service multiple top level domain path trees. Once // a virtual host is selected based on the domain, the routes are processed in order to see which // upstream cluster to route to or whether to perform a redirect. -// [#next-free-field: 21] +// [#next-free-field: 22] message VirtualHost { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.VirtualHost"; @@ -87,8 +90,15 @@ message VirtualHost { // The list of routes that will be matched, in order, for incoming requests. // The first route that matches will be used. + // Only one of this and `matcher` can be specified. repeated Route routes = 3; + // [#next-major-version: This should be included in a oneof with routes wrapped in a message.] + // The match tree to use when resolving route actions for incoming requests. Only one of this and `routes` + // can be specified. + xds.type.matcher.v3.Matcher matcher = 21 + [(xds.annotations.v3.field_status).work_in_progress = true]; + // Specifies the type of TLS enforcement the virtual host expects. If this option is not // specified, there is no TLS requirement for the virtual host. TlsRequirementType require_tls = 4 [(validate.rules).enum = {defined_only: true}]; diff --git a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst index 1b56eb354d28..9ae4b1c68585 100644 --- a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst +++ b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst @@ -16,6 +16,9 @@ better performance than the linear list matching as seen in Envoy's HTTP routing use of extension points to make it easy to extend to different inputs based on protocol or environment data as well as custom sublinear matchers and direct matchers. +Filter Integration +################## + Within supported environments (currently only HTTP filters), a wrapper proto can be used to instantiate a matching filter associated with the wrapped structure: @@ -28,7 +31,7 @@ The above example wraps a HTTP filter (the allowing us to define a match tree to be evaluated in conjunction with evaluation of the wrapped filter. Prior to data being made available to the filter, it will be provided to the match tree, which will then attempt to evaluate the matching rules with the provided data, triggering an -action if match evaluation completes in an action. +action if match evaluation results in an action. In the above example, we are specifying that we want to match on the incoming request header ``some-header`` by setting the ``input`` to @@ -54,7 +57,7 @@ the filter if ``some-header: skip_filter`` is present and ``second-header`` is s .. _arch_overview_matching_api_iteration_impact: HTTP Filter Iteration Impact -============================ +**************************** The above example only demonstrates matching on request headers, which ends up being the simplest case due to it happening before the associated filter receives any data. Matching on other HTTP @@ -80,8 +83,15 @@ client will receive an invalid response back from Envoy. If the skip action was trailers, the same gRPC-Web filter would consume all the data but never write it back out (as this happens when it sees the trailers), resulting in a gRPC-Web response with an empty body. +HTTP Routing Integration +######################## + +The matching API can be used with HTTP routing, by specifying a match tree as part of the virtual host +and specifying a Route as the resulting action. See examples in the above sections for how the match +tree can be configured. + Match Tree Validation -===================== +##################### As the match tree structure is very flexible, some filters might need to impose additional restrictions on what kind of match trees can be used. This system is somewhat inflexible at the moment, only supporting @@ -91,7 +101,7 @@ will fail during configuration load, reporting back which data input was invalid This is done for example to limit the issues talked about in :ref:`the above section ` or to help users understand in what -context a match tree can be used for a specific filter. Due to the limitations of the validations framework +context a match tree can be used for a specific filter. Due to the limitations of the validation framework at the current time, it is not used for all filters. For HTTP filters, the restrictions are specified by the filter implementation, so consult the individual diff --git a/docs/root/intro/arch_overview/http/http_routing.rst b/docs/root/intro/arch_overview/http/http_routing.rst index 816218721590..2c20dd9428f9 100644 --- a/docs/root/intro/arch_overview/http/http_routing.rst +++ b/docs/root/intro/arch_overview/http/http_routing.rst @@ -192,3 +192,63 @@ upon configuration load and cache the contents. If **response_headers_to_add** has been set for the Route or the enclosing Virtual Host, Envoy will include the specified headers in the direct HTTP response. + +Routing Via Generic Matching +---------------------------- + +Envoy recently added support for utilzing a :ref:`generic match tree ` to +specify the route table. This is a more expressive matching engine than the original one, allowing +for sublinear matching on arbitrary headers (unlike the original matching engine which could only +do this for :authority in some cases). + +To use the generic matching tree, specify a matcher on a virtual host with a RouteAction action: + +.. code-block:: yaml + + matcher: + "@type": type.googleapis.com/xds.type.matcher.v3.Matcher + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: :path + exact_match_map: + map: + "/new_endpoint/foo": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: cluster_foo + request_headers_to_add: + - header: + key: x-route-header + value: new-value + "/new_endpoint/bar": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: cluster_bar + request_headers_to_add: + - header: + key: x-route-header + value: new-value + +This allows resolving the same Route proto message used for the `routes`-based routing using the additional +matching flexibility provided by the generic matching framework. + +Note that the resulting Route also specifies a match criteria. This must be satisfied in addition to resolving +the route in order to achieve a route match. When path rewrites are used, the matched path will only depend on +the match criteria of the resolved Route. Path matching done during the match tree traversal does not contribute +to path rewrites. + +The only inputs supported are request headers (via `envoy.type.matcher.v3.HttpRequestHeaderMatchInput`). See +the docs for the :ref:`matching API ` for more information about the API as a whole. diff --git a/source/common/matcher/matcher.h b/source/common/matcher/matcher.h index 8afa2a0b9639..c81a75a18c5a 100644 --- a/source/common/matcher/matcher.h +++ b/source/common/matcher/matcher.h @@ -25,12 +25,13 @@ namespace Matcher { template class ActionBase : public Action { public: - ActionBase() : type_name_(ProtoType().GetTypeName()) {} + absl::string_view typeUrl() const override { return staticTypeUrl(); } - absl::string_view typeUrl() const override { return type_name_; } + static absl::string_view staticTypeUrl() { + const static std::string typeUrl = ProtoType().GetTypeName(); -private: - const std::string type_name_; + return typeUrl; + } }; struct MaybeMatchResult { @@ -72,9 +73,9 @@ template using DataInputFactoryCb = std::function class MatchTreeFactory { public: MatchTreeFactory(ActionFactoryContext& context, - Server::Configuration::ServerFactoryContext& server_factory_context, + Server::Configuration::ServerFactoryContext& factory_context, MatchTreeValidationVisitor& validation_visitor) - : action_factory_context_(context), server_factory_context_(server_factory_context), + : action_factory_context_(context), server_factory_context_(factory_context), validation_visitor_(validation_visitor) {} // TODO(snowp): Remove this type parameter once we only have one Matcher proto. diff --git a/source/common/router/BUILD b/source/common/router/BUILD index 4e0bbe914570..40ba40b9753c 100644 --- a/source/common/router/BUILD +++ b/source/common/router/BUILD @@ -61,10 +61,13 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/http:path_utility_lib", "//source/common/http:utility_lib", + "//source/common/http/matching:data_impl_lib", + "//source/common/matcher:matcher_lib", "//source/common/protobuf:utility_lib", "//source/common/tracing:http_tracer_lib", "//source/common/upstream:retry_factory_lib", "//source/extensions/filters/http/common:utility_lib", + "@envoy_api//envoy/config/common/matcher/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc index d756c607966a..4dbfd1555072 100644 --- a/source/common/router/config_impl.cc +++ b/source/common/router/config_impl.cc @@ -8,11 +8,15 @@ #include #include +#include "envoy/config/common/matcher/v3/matcher.pb.h" +#include "envoy/config/common/matcher/v3/matcher.pb.validate.h" #include "envoy/config/core/v3/base.pb.h" #include "envoy/config/route/v3/route.pb.h" #include "envoy/config/route/v3/route_components.pb.h" #include "envoy/http/header_map.h" #include "envoy/runtime/runtime.h" +#include "envoy/type/matcher/v3/http_inputs.pb.h" +#include "envoy/type/matcher/v3/http_inputs.pb.validate.h" #include "envoy/type/matcher/v3/string.pb.h" #include "envoy/type/v3/percent.pb.h" #include "envoy/upstream/cluster_manager.h" @@ -29,8 +33,10 @@ #include "source/common/config/utility.h" #include "source/common/config/well_known_names.h" #include "source/common/http/headers.h" +#include "source/common/http/matching/data_impl.h" #include "source/common/http/path_utility.h" #include "source/common/http/utility.h" +#include "source/common/matcher/matcher.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" #include "source/common/router/reset_header_parser.h" @@ -60,6 +66,69 @@ void mergeTransforms(Http::HeaderTransforms& dest, const Http::HeaderTransforms& src.headers_to_remove.end()); } +RouteEntryImplBaseConstSharedPtr createAndValidateRoute( + const envoy::config::route::v3::Route& route_config, const VirtualHostImpl& vhost, + const OptionalHttpFilters& optional_http_filters, + Server::Configuration::ServerFactoryContext& factory_context, + ProtobufMessage::ValidationVisitor& validator, + const absl::optional& validation_clusters) { + + RouteEntryImplBaseConstSharedPtr route; + switch (route_config.match().path_specifier_case()) { + case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kPrefix: { + route = std::make_shared(vhost, route_config, optional_http_filters, + factory_context, validator); + break; + } + case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kPath: { + route = std::make_shared(vhost, route_config, optional_http_filters, + factory_context, validator); + break; + } + case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kSafeRegex: { + route = std::make_shared(vhost, route_config, optional_http_filters, + factory_context, validator); + break; + } + case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kConnectMatcher: { + route = std::make_shared(vhost, route_config, optional_http_filters, + factory_context, validator); + break; + } + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } + + if (validation_clusters.has_value()) { + route->validateClusters(*validation_clusters); + for (const auto& shadow_policy : route->shadowPolicies()) { + ASSERT(!shadow_policy->cluster().empty()); + if (!validation_clusters->hasCluster(shadow_policy->cluster())) { + throw EnvoyException( + fmt::format("route: unknown shadow cluster '{}'", shadow_policy->cluster())); + } + } + } + + return route; +} + +class RouteActionValidationVisitor + : public Matcher::MatchTreeValidationVisitor { +public: + absl::Status performDataInputValidation(const Matcher::DataInputFactory&, + absl::string_view type_url) override { + static std::string request_header_input_name = TypeUtil::descriptorFullNameToTypeUrl( + envoy::type::matcher::v3::HttpRequestHeaderMatchInput::descriptor()->full_name()); + if (type_url == request_header_input_name) { + return absl::OkStatus(); + } + + return absl::InvalidArgumentError( + fmt::format("Route table can only match on request headers, saw {}", type_url)); + } +}; + const envoy::config::route::v3::WeightedCluster::ClusterWeight& validateWeightedClusterSpecifier( const envoy::config::route::v3::WeightedCluster::ClusterWeight& cluster) { if (!cluster.name().empty() && !cluster.cluster_header().empty()) { @@ -1319,41 +1388,27 @@ VirtualHostImpl::VirtualHostImpl( hedge_policy_ = virtual_host.hedge_policy(); } - for (const auto& route : virtual_host.routes()) { - switch (route.match().path_specifier_case()) { - case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kPrefix: { - routes_.emplace_back(new PrefixRouteEntryImpl(*this, route, optional_http_filters, - factory_context, validator)); - break; - } - case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kPath: { - routes_.emplace_back( - new PathRouteEntryImpl(*this, route, optional_http_filters, factory_context, validator)); - break; - } - case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kSafeRegex: { - routes_.emplace_back( - new RegexRouteEntryImpl(*this, route, optional_http_filters, factory_context, validator)); - break; - } - case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kConnectMatcher: { - routes_.emplace_back(new ConnectRouteEntryImpl(*this, route, optional_http_filters, - factory_context, validator)); - break; - } - default: - NOT_REACHED_GCOVR_EXCL_LINE; - } + if (virtual_host.has_matcher() && !virtual_host.routes().empty()) { + throw EnvoyException("cannot set both matcher and routes on virtual host"); + } - if (validation_clusters.has_value()) { - routes_.back()->validateClusters(*validation_clusters); - for (const auto& shadow_policy : routes_.back()->shadowPolicies()) { - ASSERT(!shadow_policy->cluster().empty()); - if (!validation_clusters->hasCluster(shadow_policy->cluster())) { - throw EnvoyException( - fmt::format("route: unknown shadow cluster '{}'", shadow_policy->cluster())); - } - } + if (virtual_host.has_matcher()) { + RouteActionContext context{*this, optional_http_filters, factory_context}; + RouteActionValidationVisitor validation_visitor; + Matcher::MatchTreeFactory factory( + context, factory_context, validation_visitor); + + matcher_ = factory.create(virtual_host.matcher())(); + + if (!validation_visitor.errors().empty()) { + // TODO(snowp): Output all violations. + throw EnvoyException(fmt::format("requirement violation while creating route match tree: {}", + validation_visitor.errors()[0])); + } + } else { + for (const auto& route : virtual_host.routes()) { + routes_.emplace_back(createAndValidateRoute(route, *this, optional_http_filters, + factory_context, validator, validation_clusters)); } } @@ -1477,33 +1532,59 @@ RouteConstSharedPtr VirtualHostImpl::getRouteFromEntries(const RouteCallback& cb return SSL_REDIRECT_ROUTE; } - // Check for a route that matches the request. - for (auto route = routes_.begin(); route != routes_.end(); ++route) { - if (!headers.Path() && !(*route)->supportsPathlessHeaders()) { - continue; - } + if (matcher_) { + Http::Matching::HttpMatchingDataImpl data; + data.onRequestHeaders(headers); - RouteConstSharedPtr route_entry = (*route)->matches(headers, stream_info, random_value); - if (nullptr == route_entry) { - continue; + auto match = Matcher::evaluateMatch(*matcher_, data); + + if (match.result_) { + // The only possible action that can be used within the route matching context + // is the RouteMatchAction, so this must be true. + ASSERT(match.result_->typeUrl() == RouteMatchAction::staticTypeUrl()); + ASSERT(dynamic_cast(match.result_.get())); + const RouteMatchAction& route_action = static_cast(*match.result_); + + if (route_action.route()->matches(headers, stream_info, random_value)) { + return route_action.route(); + } + + ENVOY_LOG(debug, "route was resolved but final route did not match incoming request"); + return nullptr; } - if (cb) { - RouteEvalStatus eval_status = (std::next(route) == routes_.end()) - ? RouteEvalStatus::NoMoreRoutes - : RouteEvalStatus::HasMoreRoutes; - RouteMatchStatus match_status = cb(route_entry, eval_status); - if (match_status == RouteMatchStatus::Accept) { - return route_entry; + ENVOY_LOG(debug, "failed to match incoming request: {}", match.match_state_); + + return nullptr; + } else { + // Check for a route that matches the request. + for (auto route = routes_.begin(); route != routes_.end(); ++route) { + if (!headers.Path() && !(*route)->supportsPathlessHeaders()) { + continue; } - if (match_status == RouteMatchStatus::Continue && - eval_status == RouteEvalStatus::NoMoreRoutes) { - return nullptr; + + RouteConstSharedPtr route_entry = (*route)->matches(headers, stream_info, random_value); + if (nullptr == route_entry) { + continue; } - continue; - } - return route_entry; + if (cb) { + RouteEvalStatus eval_status = (std::next(route) == routes_.end()) + ? RouteEvalStatus::NoMoreRoutes + : RouteEvalStatus::HasMoreRoutes; + RouteMatchStatus match_status = cb(route_entry, eval_status); + if (match_status == RouteMatchStatus::Accept) { + return route_entry; + } + if (match_status == RouteMatchStatus::Continue && + eval_status == RouteEvalStatus::NoMoreRoutes) { + return nullptr; + } + continue; + } + + return route_entry; + } } return nullptr; @@ -1671,5 +1752,18 @@ const RouteSpecificFilterConfig* PerFilterConfigs::get(const std::string& name) return it == configs_.end() ? nullptr : it->second.get(); } +Matcher::ActionFactoryCb RouteMatchActionFactory::createActionFactoryCb( + const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { + const auto& route_config = + MessageUtil::downcastAndValidate(config, + validation_visitor); + auto route = createAndValidateRoute(route_config, context.vhost, context.optional_http_filters, + context.factory_context, validation_visitor, absl::nullopt); + + return [route]() { return std::make_unique(route); }; +} +REGISTER_FACTORY(RouteMatchActionFactory, Matcher::ActionFactory); + } // namespace Router } // namespace Envoy diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h index 659861f3b11c..4b6126587d24 100644 --- a/source/common/router/config_impl.h +++ b/source/common/router/config_impl.h @@ -13,6 +13,7 @@ #include "envoy/config/core/v3/base.pb.h" #include "envoy/config/route/v3/route.pb.h" #include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/config/route/v3/route_components.pb.validate.h" #include "envoy/router/router.h" #include "envoy/runtime/runtime.h" #include "envoy/server/filter_config.h" @@ -23,6 +24,7 @@ #include "source/common/config/metadata.h" #include "source/common/http/hash_policy.h" #include "source/common/http/header_utility.h" +#include "source/common/matcher/matcher.h" #include "source/common/router/config_utility.h" #include "source/common/router/header_formatter.h" #include "source/common/router/header_parser.h" @@ -189,7 +191,7 @@ class ConfigImpl; /** * Holds all routing configuration for an entire virtual host. */ -class VirtualHostImpl : public VirtualHost { +class VirtualHostImpl : public VirtualHost, Logger::Loggable { public: VirtualHostImpl( const envoy::config::route::v3::VirtualHost& virtual_host, @@ -281,6 +283,7 @@ class VirtualHostImpl : public VirtualHost { absl::optional retry_policy_; absl::optional hedge_policy_; const CatchAllVirtualCluster virtual_cluster_catch_all_; + Matcher::MatchTreeSharedPtr matcher_; }; using VirtualHostSharedPtr = std::shared_ptr; @@ -1059,6 +1062,37 @@ class ConnectRouteEntryImpl : public RouteEntryImplBase { bool supportsPathlessHeaders() const override { return true; } }; + +// Contextual information used to construct the route actions for a match tree. +struct RouteActionContext { + const VirtualHostImpl& vhost; + const OptionalHttpFilters& optional_http_filters; + Server::Configuration::ServerFactoryContext& factory_context; +}; + +// Action used with the matching tree to specify route to use for an incoming stream. +class RouteMatchAction : public Matcher::ActionBase { +public: + explicit RouteMatchAction(RouteEntryImplBaseConstSharedPtr route) : route_(std::move(route)) {} + + RouteEntryImplBaseConstSharedPtr route() const { return route_; } + +private: + const RouteEntryImplBaseConstSharedPtr route_; +}; + +// Registered factory for RouteMatchAction. +class RouteMatchActionFactory : public Matcher::ActionFactory { +public: + Matcher::ActionFactoryCb + createActionFactoryCb(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; + std::string name() const override { return "route"; } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + /** * Wraps the route configuration which matches an incoming request headers to a backend cluster. * This is split out mainly to help with unit testing. diff --git a/test/common/router/config_impl_test.cc b/test/common/router/config_impl_test.cc index c540db9e123f..a5060fcc534b 100644 --- a/test/common/router/config_impl_test.cc +++ b/test/common/router/config_impl_test.cc @@ -1215,6 +1215,168 @@ TEST_F(RouteMatcherTest, TestRoutesWithInvalidVirtualCluster) { "virtual clusters must define 'headers'"); } +// Validates basic usage of the match tree to resolve route actions. +TEST_F(RouteMatcherTest, TestMatchTree) { + const std::string yaml = R"EOF( +virtual_hosts: +- name: www2 + domains: + - lyft.com + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: :path + exact_match_map: + map: + "/new_endpoint/foo": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: root_ww2 + request_headers_to_add: + - header: + key: x-route-header + value: match_tree + "/new_endpoint/bar": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: root_ww2 + request_headers_to_add: + - header: + key: x-route-header + value: match_tree_2 + "/new_endpoint/baz": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: /something/else + route: + cluster: root_ww2 + request_headers_to_add: + - header: + key: x-route-header + value: match_tree_2 + )EOF"; + + NiceMock stream_info; + factory_context_.cluster_manager_.initializeClusters( + {"www2", "root_www2", "www2_staging", "instant-server"}, {}); + TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true); + + { + Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/foo", "GET"); + const RouteEntry* route = config.route(headers, 0)->routeEntry(); + route->finalizeRequestHeaders(headers, stream_info, true); + EXPECT_EQ("match_tree", headers.get_("x-route-header")); + } + + { + Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/bar", "GET"); + const RouteEntry* route = config.route(headers, 0)->routeEntry(); + route->finalizeRequestHeaders(headers, stream_info, true); + EXPECT_EQ("match_tree_2", headers.get_("x-route-header")); + } + Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/baz", "GET"); + EXPECT_EQ(nullptr, config.route(headers, 0)); +} + +// Validates that we fail creating a route config if an invalid data input is used. +TEST_F(RouteMatcherTest, TestMatchInvalidInput) { + const std::string yaml = R"EOF( +virtual_hosts: +- name: www2 + domains: + - lyft.com + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput + header_name: :path + exact_match_map: + map: + "/new_endpoint/foo": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: root_ww2 + request_headers_to_add: + - header: + key: x-route-header + value: match_tree + )EOF"; + + NiceMock stream_info; + factory_context_.cluster_manager_.initializeClusters( + {"www2", "root_www2", "www2_staging", "instant-server"}, {}); + EXPECT_THROW_WITH_MESSAGE( + TestConfigImpl(parseRouteConfigurationFromYaml(yaml), factory_context_, true), EnvoyException, + "requirement violation while creating route match tree: INVALID_ARGUMENT: Route table can " + "only match on request headers, saw " + "type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput"); +} + +// Validates that we fail creating a route config if an invalid data input is used. +TEST_F(RouteMatcherTest, TestMatchInvalidInputTwoMatchers) { + const std::string yaml = R"EOF( +virtual_hosts: +- name: www2 + domains: + - lyft.com + routes: + - match: { prefix: "/" } + route: { cluster: "regex" } + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: :path + exact_match_map: + map: + "/new_endpoint/foo": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: root_ww2 + request_headers_to_add: + - header: + key: x-route-header + value: match_tree + )EOF"; + + NiceMock stream_info; + factory_context_.cluster_manager_.initializeClusters( + {"www2", "root_www2", "www2_staging", "instant-server"}, {}); + EXPECT_THROW_WITH_MESSAGE( + TestConfigImpl(parseRouteConfigurationFromYaml(yaml), factory_context_, true), EnvoyException, + "cannot set both matcher and routes on virtual host"); +} + // Validates behavior of request_headers_to_add at router, vhost, and route levels. TEST_F(RouteMatcherTest, TestAddRemoveRequestHeaders) { const std::string yaml = R"EOF( diff --git a/test/integration/integration_test.cc b/test/integration/integration_test.cc index 06cee2599e1b..6b036954ea21 100644 --- a/test/integration/integration_test.cc +++ b/test/integration/integration_test.cc @@ -559,6 +559,55 @@ name: matcher second_codec->close(); } +// Verifies routing via the match tree API. +TEST_P(IntegrationTest, MatchTreeRouting) { + config_helper_.addRuntimeOverride("envoy.reloadable_features.experimental_matching_api", "true"); + + const std::string vhost_yaml = R"EOF( + name: vhost + domains: ["matcher.com"] + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: match-header + exact_match_map: + map: + "route": + action: + name: route + typed_config: + "@type": type.googleapis.com/envoy.config.route.v3.Route + match: + prefix: / + route: + cluster: cluster_0 + )EOF"; + + envoy::config::route::v3::VirtualHost virtual_host; + TestUtility::loadFromYaml(vhost_yaml, virtual_host); + + config_helper_.addVirtualHost(virtual_host); + autonomous_upstream_ = true; + + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + Http::TestRequestHeaderMapImpl headers{{":method", "GET"}, + {":path", "/whatever"}, + {":scheme", "http"}, + {"match-header", "route"}, + {":authority", "matcher.com"}}; + auto response = codec_client_->makeHeaderOnlyRequest(headers); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_THAT(response->headers(), HttpStatusIs("200")); + + codec_client_->close(); +} + // This is a regression for https://github.com/envoyproxy/envoy/issues/2715 and validates that a // pending request is not sent on a connection that has been half-closed. TEST_P(IntegrationTest, UpstreamDisconnectWithTwoRequests) { diff --git a/test/per_file_coverage.sh b/test/per_file_coverage.sh index e4813c691723..1c3c49d93caa 100755 --- a/test/per_file_coverage.sh +++ b/test/per_file_coverage.sh @@ -15,7 +15,7 @@ declare -a KNOWN_LOW_COVERAGE=( "source/common/http:96.3" "source/common/http/http2:96.4" "source/common/json:90.1" -"source/common/matcher:94.2" +"source/common/matcher:94.0" "source/common/network:94.4" # Flaky, `activateFileEvents`, `startSecureTransport` and `ioctl`, listener_socket do not always report LCOV "source/common/protobuf:95.3" "source/common/quic:91.8"