Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add header matched routing #110

Merged
merged 12 commits into from
Oct 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/configuration/http_conn_man/route_config/route.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ next (e.g., redirect, forward, rewrite, etc.).
"retry_policy": "{...}",
"rate_limit": "{...}",
"shadow": "{...}",
"priority": "..."
"priority": "...",
"headers": "[]"
}

prefix
Expand Down Expand Up @@ -94,6 +95,9 @@ priority
*(optional, string)* Optionally specifies the :ref:`routing priority
<arch_overview_http_routing_priority>`.

:ref:`headers <config_http_conn_man_route_table_route_headers>`
*(optional, array)* Specifies a set of headers that the route should match on.

.. _config_http_conn_man_route_table_route_runtime:

Runtime
Expand Down Expand Up @@ -197,3 +201,27 @@ runtime_key
from 0 to 10000, allowing for increments of 0.01% of requests to be shadowed. If the runtime key
is specified in the configuration but not present in runtime, 0 is the default and thus 0% of
requests will be shadowed.

.. _config_http_conn_man_route_table_route_headers:

Headers
-------

The router can match a request to a route based on headers specified in the route config.

.. code-block:: json

[
{"name": "...", "value": "..."}
]

name
*(required, string)* Specifies the name of the header in the request.

value
*(optional, string)* Specifies the value of the header. If the value is absent a request that has the
``name`` header will match, regardless of the header's value.

The router will check the request's headers against all the specified
headers in the route config. A match will happen if all the headers in the route are present in
the request with the same values (or based on presence if the ``value`` field is not in the config).
2 changes: 1 addition & 1 deletion include/envoy/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class RouteEntry {
* Do potentially destructive header transforms on request headers prior to forwarding. For
* example URL prefix rewriting, adding headers, etc. This should only be called ONCE
* immediately prior to forwarding. It is done this way vs. copying for performance reasons.
* @param headers supplise the request headers, which may be modified during this call.
* @param headers supplies the request headers, which may be modified during this call.
*/
virtual void finalizeRequestHeaders(Http::HeaderMap& headers) const PURE;

Expand Down
22 changes: 22 additions & 0 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ RouteEntryImplBase::RouteEntryImplBase(const VirtualHost& vhost, const Json::Obj
if (!(isRedirect() ^ !cluster_name_.empty())) {
throw EnvoyException("routes must be either redirects or cluster targets");
}

if (route.hasObject("headers")) {
std::vector<Json::Object> config_headers = route.getObjectArray("headers");
for (const Json::Object& header_map : config_headers) {
// allow header value to be empty, allows matching to be only based on header presence.
config_headers_.emplace_back(Http::LowerCaseString(header_map.getString("name")),
header_map.getString("value", EMPTY_STRING));
}
}
}

bool RouteEntryImplBase::matches(const Http::HeaderMap& headers, uint64_t random_value) const {
Expand All @@ -79,6 +88,19 @@ bool RouteEntryImplBase::matches(const Http::HeaderMap& headers, uint64_t random
matches &= (headers.get(Http::Headers::get().ContentType) == content_type_);
}

if (!config_headers_.empty()) {
for (const HeaderData& header_data : config_headers_) {
if (header_data.value_ == EMPTY_STRING) {
matches &= headers.has(header_data.name_);
} else {
matches &= (headers.get(header_data.name_) == header_data.value_);
}
if (!matches) {
break;
}
}
}

return matches;
}

Expand Down
9 changes: 9 additions & 0 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ class RouteEntryImplBase : public RouteEntry, public Matchable, public RedirectE
uint64_t default_;
};

struct HeaderData {
HeaderData(const Http::LowerCaseString& name, const std::string& value)
: name_(name), value_(value) {}

const Http::LowerCaseString name_;
const std::string value_;
};

static Optional<RuntimeData> loadRuntimeData(const Json::Object& route);

// Default timeout is 15s if nothing is specified in the route config.
Expand All @@ -211,6 +219,7 @@ class RouteEntryImplBase : public RouteEntry, public Matchable, public RedirectE
const RateLimitPolicyImpl rate_limit_policy_;
const ShadowPolicyImpl shadow_policy_;
const Upstream::ResourcePriority priority_;
std::vector<HeaderData> config_headers_;
};

/**
Expand Down
78 changes: 78 additions & 0 deletions test/common/router/config_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,84 @@ TEST(RouteMatcherTest, Priority) {
}
}

TEST(RouteMatcherTest, HeaderMatchedRouting) {
std::string json = R"EOF(
{
"virtual_hosts": [
{
"name": "local_service",
"domains": ["*"],
"routes": [
{
"prefix": "/",
"cluster": "local_service_with_headers",
"headers" : [
{"name": "test_header", "value": "test"}
]
},
{
"prefix": "/",
"cluster": "local_service_with_multiple_headers",
"headers" : [
{"name": "test_header_multiple1", "value": "test1"},
{"name": "test_header_multiple2", "value": "test2"}
]
},
{
"prefix": "/",
"cluster": "local_service_with_empty_headers",
"headers" : [
{"name": "test_header_presence"}
]
},
{
"prefix": "/",
"cluster": "local_service_without_headers"
}
]
}
]
}
)EOF";

Json::StringLoader loader(json);
NiceMock<Runtime::MockLoader> runtime;
NiceMock<Upstream::MockClusterManager> cm;
ConfigImpl config(loader, runtime, cm);

{
EXPECT_EQ("local_service_without_headers",
config.routeForRequest(genHeaders("www.lyft.com", "/", "GET"), 0)->clusterName());
}

{
Http::HeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET");
headers.addViaCopy("test_header", "test");
EXPECT_EQ("local_service_with_headers", config.routeForRequest(headers, 0)->clusterName());
}

{
Http::HeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET");
headers.addViaCopy("test_header_multiple1", "test1");
headers.addViaCopy("test_header_multiple2", "test2");
EXPECT_EQ("local_service_with_multiple_headers",
config.routeForRequest(headers, 0)->clusterName());
}

{
Http::HeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET");
headers.addViaCopy("non_existent_header", "foo");
EXPECT_EQ("local_service_without_headers", config.routeForRequest(headers, 0)->clusterName());
}

{
Http::HeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET");
headers.addViaCopy("test_header_presence", "test");
EXPECT_EQ("local_service_with_empty_headers",
config.routeForRequest(headers, 0)->clusterName());
}
}

TEST(RouteMatcherTest, ContentType) {
std::string json = R"EOF(
{
Expand Down