Skip to content

Commit

Permalink
extensions/thrift_proxy: Add header matching to thrift router (#4239)
Browse files Browse the repository at this point in the history
This change adds header matching to the thrift router We do this by pulling in the route proto definition into the thrift route proto and making use of the Http::HeaderUtility class to do the matching for us. As such, we support the same type of header matching that exists for the http router.

Risk Level: LOW
Testing: unit and integrations tests, new and old, pass.
Doc changes: api docs updated
Release notes: n/a

Signed-off-by: Brian Ramos <[email protected]>
  • Loading branch information
brirams authored and zuercher committed Aug 27, 2018
1 parent c9ce5d2 commit f5e219e
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 45 deletions.
3 changes: 3 additions & 0 deletions api/envoy/config/filter/network/thrift_proxy/v2alpha1/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ api_proto_library_internal(
"route.proto",
"thrift_proxy.proto",
],
deps = [
"//envoy/api/v2/route",
],
)
24 changes: 21 additions & 3 deletions api/envoy/config/filter/network/thrift_proxy/v2alpha1/route.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ syntax = "proto3";
package envoy.config.filter.network.thrift_proxy.v2alpha1;
option go_package = "v2";

import "envoy/api/v2/route/route.proto";
import "validate/validate.proto";
import "gogoproto/gogo.proto";

Expand All @@ -28,7 +29,7 @@ message Route {
RouteAction route = 2 [(validate.rules).message.required = true, (gogoproto.nullable) = false];
}

// [#comment:next free field: 4]
// [#comment:next free field: 5]
message RouteMatch {
oneof match_specifier {
option (validate.required) = true;
Expand All @@ -43,9 +44,26 @@ message RouteMatch {
string service_name = 2;
}

// Inverts whatever matching is done in match_specifier. Cannot be combined with wildcard matching
// as that would result in routes never being matched.
// Inverts whatever matching is done in the :ref:`method_name
// <envoy_api_field_config.filter.network.thrift_proxy.v2alpha1.RouteMatch.method_name>` or
// :ref:`service_name
// <envoy_api_field_config.filter.network.thrift_proxy.v2alpha1.RouteMatch.service_name>` fields.
// Cannot be combined with wildcard matching as that would result in routes never being matched.
//
// .. note::
//
// This does not invert matching done as part of the :ref:`headers field
// <envoy_api_field_config.filter.network.thrift_proxy.v2alpha1.RouteMatch.headers>` field. To
// invert header matching, see :ref:`invert_match
// <envoy_api_field_route.HeaderMatcher.invert_match>`.
bool invert = 3;

// Specifies a set of headers that the route should match on. 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). Note that this only applies for Thrift transports and/or
// protocols that support headers.
repeated envoy.api.v2.route.HeaderMatcher headers = 4;
}

// [#comment:next free field: 2]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ envoy_cc_library(
"//include/envoy/upstream:load_balancer_interface",
"//include/envoy/upstream:thread_local_cluster_interface",
"//source/common/common:logger_lib",
"//source/common/http:header_utility_lib",
"//source/common/upstream:load_balancer_lib",
"//source/extensions/filters/network:well_known_names",
"//source/extensions/filters/network/thrift_proxy:app_exception_lib",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,22 @@ namespace Router {

RouteEntryImplBase::RouteEntryImplBase(
const envoy::config::filter::network::thrift_proxy::v2alpha1::Route& route)
: cluster_name_(route.route().cluster()) {}
: cluster_name_(route.route().cluster()) {
for (const auto& header_map : route.match().headers()) {
config_headers_.push_back(header_map);
}
}

const std::string& RouteEntryImplBase::clusterName() const { return cluster_name_; }

const RouteEntry* RouteEntryImplBase::routeEntry() const { return this; }

RouteConstSharedPtr RouteEntryImplBase::clusterEntry() const { return shared_from_this(); }

bool RouteEntryImplBase::headersMatch(const Http::HeaderMap& headers) const {
return Http::HeaderUtility::matchHeaders(headers, config_headers_);
}

MethodNameRouteEntryImpl::MethodNameRouteEntryImpl(
const envoy::config::filter::network::thrift_proxy::v2alpha1::Route& route)
: RouteEntryImplBase(route), method_name_(route.match().method_name()),
Expand All @@ -35,11 +43,13 @@ MethodNameRouteEntryImpl::MethodNameRouteEntryImpl(
}

RouteConstSharedPtr MethodNameRouteEntryImpl::matches(const MessageMetadata& metadata) const {
bool matches =
method_name_.empty() || (metadata.hasMethodName() && metadata.methodName() == method_name_);
if (RouteEntryImplBase::headersMatch(metadata.headers())) {
bool matches =
method_name_.empty() || (metadata.hasMethodName() && metadata.methodName() == method_name_);

if (matches ^ invert_) {
return clusterEntry();
if (matches ^ invert_) {
return clusterEntry();
}
}

return nullptr;
Expand All @@ -61,12 +71,14 @@ ServiceNameRouteEntryImpl::ServiceNameRouteEntryImpl(
}

RouteConstSharedPtr ServiceNameRouteEntryImpl::matches(const MessageMetadata& metadata) const {
bool matches = service_name_.empty() ||
(metadata.hasMethodName() &&
StringUtil::startsWith(metadata.methodName().c_str(), service_name_));
if (RouteEntryImplBase::headersMatch(metadata.headers())) {
bool matches = service_name_.empty() ||
(metadata.hasMethodName() &&
StringUtil::startsWith(metadata.methodName().c_str(), service_name_));

if (matches ^ invert_) {
return clusterEntry();
if (matches ^ invert_) {
return clusterEntry();
}
}

return nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "envoy/upstream/load_balancer.h"

#include "common/common/logger.h"
#include "common/http/header_utility.h"
#include "common/upstream/load_balancer_impl.h"

#include "extensions/filters/network/thrift_proxy/conn_manager.h"
Expand Down Expand Up @@ -39,9 +40,11 @@ class RouteEntryImplBase : public RouteEntry,

protected:
RouteConstSharedPtr clusterEntry() const;
bool headersMatch(const Http::HeaderMap& headers) const;

private:
const std::string cluster_name_;
std::vector<Http::HeaderUtility::HeaderData> config_headers_;
};

typedef std::shared_ptr<const RouteEntryImplBase> RouteEntryImplBaseConstSharedPtr;
Expand Down
15 changes: 14 additions & 1 deletion test/extensions/filters/network/thrift_proxy/driver/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ def main(cfg, reqhandle, resphandle):
transport,
client_type=THeaderTransport.CLIENT_TYPE.HEADER,
)

if cfg.headers is not None:
pairs = cfg.headers.split(",")
for p in pairs:
key,value=p.split("=")
transport.set_header(key,value)

if cfg.protocol == "binary":
transport.set_protocol_id(THeaderTransport.T_BINARY_PROTOCOL)
elif cfg.protocol == "compact":
Expand Down Expand Up @@ -159,7 +166,6 @@ def main(cfg, reqhandle, resphandle):

transport.close()


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Thrift client tool.",
Expand Down Expand Up @@ -225,6 +231,13 @@ def main(cfg, reqhandle, resphandle):
dest="unix",
action="store_true",
)
parser.add_argument(
"--headers",
dest="headers",
metavar="KEY=VALUE[,KEY=VALUE]",
help="list of comma-delimited, key value pairs to include as tranport headers.",
)

cfg = parser.parse_args()

reqhandle = io.BytesIO()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

# Generates request and response fixtures for integration tests.

# Usage: generate_fixture.sh <transport> <protocol> [multiplex-service] -- method [param...]
# Usage: generate_fixture.sh <transport> <protocol> -s [multiplex-service] -H [headers] method [param...]

set -e

function usage() {
echo "Usage: $0 <mode> <transport> <protocol> [multiplex-service] -- method [param...]"
echo "Usage: $0 <mode> <transport> <protocol> -s [multiplex-service] -H [headers] method [param...]"
echo "where mode is success, exception, or idl-exception"
exit 1
}
Expand All @@ -24,24 +24,37 @@ fi
MODE="$1"
TRANSPORT="$2"
PROTOCOL="$3"
MULTIPLEX="$4"
if ! shift 4; then

if ! shift 3; then
usage
fi

if [[ -z "${MODE}" || -z "${TRANSPORT}" || -z "${PROTOCOL}" || -z "${MULTIPLEX}" ]]; then
if [[ -z "${MODE}" || -z "${TRANSPORT}" || -z "${PROTOCOL}" ]]; then
usage
fi

if [[ "${MULTIPLEX}" != "--" ]]; then
if [[ "$1" != "--" ]]; then
echo "expected -- after multiplex service name"
exit 1
fi
shift
else
MULTIPLEX=""
fi
MULTIPLEX=
HEADERS=
while getopts ":s:H:" opt; do
case ${opt} in
s)
MULTIPLEX=$OPTARG
;;
H)
HEADERS=$OPTARG
;;

\?)
echo "Invalid Option: -$OPTARG" >&2
exit 1
;;
:)
echo "Invalid Option: -$OPTARG requires an argument" >&2
exit 1
;;
esac
done
shift $((OPTIND -1))

METHOD="$1"
if [[ "${METHOD}" == "" ]]; then
Expand All @@ -59,8 +72,8 @@ SERVICE_FLAGS=("--addr" "${SOCKET}"
"--protocol" "${PROTOCOL}")

if [[ -n "$MULTIPLEX" ]]; then
SERVICE_FLAGS[9]="--multiplex"
SERVICE_FLAGS[10]="${MULTIPLEX}"
SERVICE_FLAGS+=("--multiplex")
SERVICE_FLAGS+=("${MULTIPLEX}")

REQUEST_FILE="${FIXTURE_DIR}/${TRANSPORT}-${PROTOCOL}-${MULTIPLEX}-${MODE}.request"
RESPONSE_FILE="${FIXTURE_DIR}/${TRANSPORT}-${PROTOCOL}-${MULTIPLEX}-${MODE}.response"
Expand All @@ -84,6 +97,11 @@ while [[ ! -a "${SOCKET}" ]]; do
fi
done

if [[ -n "$HEADERS" ]]; then
SERVICE_FLAGS+=("--headers")
SERVICE_FLAGS+=("$HEADERS")
fi

"${DRIVER_DIR}/client" "${SERVICE_FLAGS[@]}" \
--request "${REQUEST_FILE}" \
--response "${RESPONSE_FILE}" \
Expand Down
14 changes: 13 additions & 1 deletion test/extensions/filters/network/thrift_proxy/integration.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,21 @@ void BaseThriftIntegrationTest::preparePayloads(const PayloadOptions& options,
};

if (options.service_name_) {
args.push_back("-s");
args.push_back(*options.service_name_);
}
args.push_back("--");

if (options.headers_.size() > 0) {
args.push_back("-H");

std::vector<std::string> headers;
std::transform(options.headers_.begin(), options.headers_.end(), std::back_inserter(headers),
[](const std::pair<std::string, std::string>& header) -> std::string {
return header.first + "=" + header.second;
});
args.push_back(StringUtil::join(headers, ","));
}

args.push_back(options.method_name_);
std::copy(options.method_args_.begin(), options.method_args_.end(), std::back_inserter(args));

Expand Down
6 changes: 4 additions & 2 deletions test/extensions/filters/network/thrift_proxy/integration.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ enum class DriverMode {
struct PayloadOptions {
PayloadOptions(TransportType transport, ProtocolType protocol, DriverMode mode,
absl::optional<std::string> service_name, std::string method_name,
std::vector<std::string> method_args = {})
std::vector<std::string> method_args = {},
std::vector<std::pair<std::string, std::string>> headers = {})
: transport_(transport), protocol_(protocol), mode_(mode), service_name_(service_name),
method_name_(method_name), method_args_(method_args) {}
method_name_(method_name), method_args_(method_args), headers_(headers) {}

std::string modeName() const;
std::string transportName() const;
Expand All @@ -43,6 +44,7 @@ struct PayloadOptions {
const absl::optional<std::string> service_name_;
const std::string method_name_;
const std::vector<std::string> method_args_;
const std::vector<std::pair<std::string, std::string>> headers_;
};

class BaseThriftIntegrationTest : public BaseIntegrationTest {
Expand Down
50 changes: 38 additions & 12 deletions test/extensions/filters/network/thrift_proxy/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,29 @@ class ThriftConnManagerIntegrationTest
cluster: "cluster_0"
- match:
method_name: "execute"
headers:
- name: "x-header-1"
exact_match: "x-value-1"
- name: "x-header-2"
regex_match: "0.[5-9]"
- name: "x-header-3"
range_match:
start: 100
end: 200
- name: "x-header-4"
prefix_match: "user_id:"
- name: "x-header-5"
suffix_match: "asdf"
route:
cluster: "cluster_1"
- match:
method_name: "poke"
method_name: "execute"
route:
cluster: "cluster_2"
- match:
method_name: "poke"
route:
cluster: "cluster_3"
)EOF";
}

Expand All @@ -51,7 +68,16 @@ class ThriftConnManagerIntegrationTest
service_name = "svcname";
}

PayloadOptions options(transport_, protocol_, mode, service_name, "execute");
std::vector<std::pair<std::string, std::string>> headers;
if (transport_ == TransportType::Header) {
headers.push_back(std::make_pair("x-header-1", "x-value-1"));
headers.push_back(std::make_pair("x-header-2", "0.6"));
headers.push_back(std::make_pair("x-header-3", "150"));
headers.push_back(std::make_pair("x-header-4", "user_id:10"));
headers.push_back(std::make_pair("x-header-5", "garbage_asdf"));
}

PayloadOptions options(transport_, protocol_, mode, service_name, "execute", {}, headers);
preparePayloads(options, request_bytes_, response_bytes_);
ASSERT(request_bytes_.length() > 0);
ASSERT(response_bytes_.length() > 0);
Expand All @@ -76,16 +102,14 @@ class ThriftConnManagerIntegrationTest
// We allocate as many upstreams as there are clusters, with each upstream being allocated
// to clusters in the order they're defined in the bootstrap config.
void initializeCommon() {
setUpstreamCount(3);
setUpstreamCount(4);

config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) {
auto* c1 = bootstrap.mutable_static_resources()->add_clusters();
c1->MergeFrom(bootstrap.static_resources().clusters()[0]);
c1->set_name("cluster_1");

auto* c2 = bootstrap.mutable_static_resources()->add_clusters();
c2->MergeFrom(bootstrap.static_resources().clusters()[0]);
c2->set_name("cluster_2");
for (int i = 1; i < 4; i++) {
auto* c = bootstrap.mutable_static_resources()->add_clusters();
c->MergeFrom(bootstrap.static_resources().clusters()[0]);
c->set_name(fmt::format("cluster_{}", i));
}
});

BaseThriftIntegrationTest::initialize();
Expand All @@ -101,11 +125,13 @@ class ThriftConnManagerIntegrationTest
// while oneway's are handled by the "poke" method. All other requests
// are handled by "execute".
FakeUpstream* getExpectedUpstream(bool oneway) {
int upstreamIdx = 1;
int upstreamIdx = 2;
if (multiplexed_) {
upstreamIdx = 0;
} else if (oneway) {
upstreamIdx = 2;
upstreamIdx = 3;
} else if (transport_ == TransportType::Header) {
upstreamIdx = 1;
}

return fake_upstreams_[upstreamIdx].get();
Expand Down
Loading

0 comments on commit f5e219e

Please sign in to comment.