Skip to content

Commit f5e219e

Browse files
briramszuercher
authored andcommitted
extensions/thrift_proxy: Add header matching to thrift router (envoyproxy#4239)
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]>
1 parent c9ce5d2 commit f5e219e

File tree

11 files changed

+372
-45
lines changed

11 files changed

+372
-45
lines changed

api/envoy/config/filter/network/thrift_proxy/v2alpha1/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ api_proto_library_internal(
88
"route.proto",
99
"thrift_proxy.proto",
1010
],
11+
deps = [
12+
"//envoy/api/v2/route",
13+
],
1114
)

api/envoy/config/filter/network/thrift_proxy/v2alpha1/route.proto

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ syntax = "proto3";
33
package envoy.config.filter.network.thrift_proxy.v2alpha1;
44
option go_package = "v2";
55

6+
import "envoy/api/v2/route/route.proto";
67
import "validate/validate.proto";
78
import "gogoproto/gogo.proto";
89

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

31-
// [#comment:next free field: 4]
32+
// [#comment:next free field: 5]
3233
message RouteMatch {
3334
oneof match_specifier {
3435
option (validate.required) = true;
@@ -43,9 +44,26 @@ message RouteMatch {
4344
string service_name = 2;
4445
}
4546

46-
// Inverts whatever matching is done in match_specifier. Cannot be combined with wildcard matching
47-
// as that would result in routes never being matched.
47+
// Inverts whatever matching is done in the :ref:`method_name
48+
// <envoy_api_field_config.filter.network.thrift_proxy.v2alpha1.RouteMatch.method_name>` or
49+
// :ref:`service_name
50+
// <envoy_api_field_config.filter.network.thrift_proxy.v2alpha1.RouteMatch.service_name>` fields.
51+
// Cannot be combined with wildcard matching as that would result in routes never being matched.
52+
//
53+
// .. note::
54+
//
55+
// This does not invert matching done as part of the :ref:`headers field
56+
// <envoy_api_field_config.filter.network.thrift_proxy.v2alpha1.RouteMatch.headers>` field. To
57+
// invert header matching, see :ref:`invert_match
58+
// <envoy_api_field_route.HeaderMatcher.invert_match>`.
4859
bool invert = 3;
60+
61+
// Specifies a set of headers that the route should match on. The router will check the request’s
62+
// headers against all the specified headers in the route config. A match will happen if all the
63+
// headers in the route are present in the request with the same values (or based on presence if
64+
// the value field is not in the config). Note that this only applies for Thrift transports and/or
65+
// protocols that support headers.
66+
repeated envoy.api.v2.route.HeaderMatcher headers = 4;
4967
}
5068

5169
// [#comment:next free field: 2]

source/extensions/filters/network/thrift_proxy/router/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ envoy_cc_library(
4040
"//include/envoy/upstream:load_balancer_interface",
4141
"//include/envoy/upstream:thread_local_cluster_interface",
4242
"//source/common/common:logger_lib",
43+
"//source/common/http:header_utility_lib",
4344
"//source/common/upstream:load_balancer_lib",
4445
"//source/extensions/filters/network:well_known_names",
4546
"//source/extensions/filters/network/thrift_proxy:app_exception_lib",

source/extensions/filters/network/thrift_proxy/router/router_impl.cc

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@ namespace Router {
1717

1818
RouteEntryImplBase::RouteEntryImplBase(
1919
const envoy::config::filter::network::thrift_proxy::v2alpha1::Route& route)
20-
: cluster_name_(route.route().cluster()) {}
20+
: cluster_name_(route.route().cluster()) {
21+
for (const auto& header_map : route.match().headers()) {
22+
config_headers_.push_back(header_map);
23+
}
24+
}
2125

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

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

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

32+
bool RouteEntryImplBase::headersMatch(const Http::HeaderMap& headers) const {
33+
return Http::HeaderUtility::matchHeaders(headers, config_headers_);
34+
}
35+
2836
MethodNameRouteEntryImpl::MethodNameRouteEntryImpl(
2937
const envoy::config::filter::network::thrift_proxy::v2alpha1::Route& route)
3038
: RouteEntryImplBase(route), method_name_(route.match().method_name()),
@@ -35,11 +43,13 @@ MethodNameRouteEntryImpl::MethodNameRouteEntryImpl(
3543
}
3644

3745
RouteConstSharedPtr MethodNameRouteEntryImpl::matches(const MessageMetadata& metadata) const {
38-
bool matches =
39-
method_name_.empty() || (metadata.hasMethodName() && metadata.methodName() == method_name_);
46+
if (RouteEntryImplBase::headersMatch(metadata.headers())) {
47+
bool matches =
48+
method_name_.empty() || (metadata.hasMethodName() && metadata.methodName() == method_name_);
4049

41-
if (matches ^ invert_) {
42-
return clusterEntry();
50+
if (matches ^ invert_) {
51+
return clusterEntry();
52+
}
4353
}
4454

4555
return nullptr;
@@ -61,12 +71,14 @@ ServiceNameRouteEntryImpl::ServiceNameRouteEntryImpl(
6171
}
6272

6373
RouteConstSharedPtr ServiceNameRouteEntryImpl::matches(const MessageMetadata& metadata) const {
64-
bool matches = service_name_.empty() ||
65-
(metadata.hasMethodName() &&
66-
StringUtil::startsWith(metadata.methodName().c_str(), service_name_));
74+
if (RouteEntryImplBase::headersMatch(metadata.headers())) {
75+
bool matches = service_name_.empty() ||
76+
(metadata.hasMethodName() &&
77+
StringUtil::startsWith(metadata.methodName().c_str(), service_name_));
6778

68-
if (matches ^ invert_) {
69-
return clusterEntry();
79+
if (matches ^ invert_) {
80+
return clusterEntry();
81+
}
7082
}
7183

7284
return nullptr;

source/extensions/filters/network/thrift_proxy/router/router_impl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "envoy/upstream/load_balancer.h"
1010

1111
#include "common/common/logger.h"
12+
#include "common/http/header_utility.h"
1213
#include "common/upstream/load_balancer_impl.h"
1314

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

4041
protected:
4142
RouteConstSharedPtr clusterEntry() const;
43+
bool headersMatch(const Http::HeaderMap& headers) const;
4244

4345
private:
4446
const std::string cluster_name_;
47+
std::vector<Http::HeaderUtility::HeaderData> config_headers_;
4548
};
4649

4750
typedef std::shared_ptr<const RouteEntryImplBase> RouteEntryImplBaseConstSharedPtr;

test/extensions/filters/network/thrift_proxy/driver/client.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ def main(cfg, reqhandle, resphandle):
7979
transport,
8080
client_type=THeaderTransport.CLIENT_TYPE.HEADER,
8181
)
82+
83+
if cfg.headers is not None:
84+
pairs = cfg.headers.split(",")
85+
for p in pairs:
86+
key,value=p.split("=")
87+
transport.set_header(key,value)
88+
8289
if cfg.protocol == "binary":
8390
transport.set_protocol_id(THeaderTransport.T_BINARY_PROTOCOL)
8491
elif cfg.protocol == "compact":
@@ -159,7 +166,6 @@ def main(cfg, reqhandle, resphandle):
159166

160167
transport.close()
161168

162-
163169
if __name__ == "__main__":
164170
parser = argparse.ArgumentParser(
165171
description="Thrift client tool.",
@@ -225,6 +231,13 @@ def main(cfg, reqhandle, resphandle):
225231
dest="unix",
226232
action="store_true",
227233
)
234+
parser.add_argument(
235+
"--headers",
236+
dest="headers",
237+
metavar="KEY=VALUE[,KEY=VALUE]",
238+
help="list of comma-delimited, key value pairs to include as tranport headers.",
239+
)
240+
228241
cfg = parser.parse_args()
229242

230243
reqhandle = io.BytesIO()

test/extensions/filters/network/thrift_proxy/driver/generate_fixture.sh

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
# Generates request and response fixtures for integration tests.
44

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

77
set -e
88

99
function usage() {
10-
echo "Usage: $0 <mode> <transport> <protocol> [multiplex-service] -- method [param...]"
10+
echo "Usage: $0 <mode> <transport> <protocol> -s [multiplex-service] -H [headers] method [param...]"
1111
echo "where mode is success, exception, or idl-exception"
1212
exit 1
1313
}
@@ -24,24 +24,37 @@ fi
2424
MODE="$1"
2525
TRANSPORT="$2"
2626
PROTOCOL="$3"
27-
MULTIPLEX="$4"
28-
if ! shift 4; then
27+
28+
if ! shift 3; then
2929
usage
3030
fi
3131

32-
if [[ -z "${MODE}" || -z "${TRANSPORT}" || -z "${PROTOCOL}" || -z "${MULTIPLEX}" ]]; then
32+
if [[ -z "${MODE}" || -z "${TRANSPORT}" || -z "${PROTOCOL}" ]]; then
3333
usage
3434
fi
3535

36-
if [[ "${MULTIPLEX}" != "--" ]]; then
37-
if [[ "$1" != "--" ]]; then
38-
echo "expected -- after multiplex service name"
39-
exit 1
40-
fi
41-
shift
42-
else
43-
MULTIPLEX=""
44-
fi
36+
MULTIPLEX=
37+
HEADERS=
38+
while getopts ":s:H:" opt; do
39+
case ${opt} in
40+
s)
41+
MULTIPLEX=$OPTARG
42+
;;
43+
H)
44+
HEADERS=$OPTARG
45+
;;
46+
47+
\?)
48+
echo "Invalid Option: -$OPTARG" >&2
49+
exit 1
50+
;;
51+
:)
52+
echo "Invalid Option: -$OPTARG requires an argument" >&2
53+
exit 1
54+
;;
55+
esac
56+
done
57+
shift $((OPTIND -1))
4558

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

6174
if [[ -n "$MULTIPLEX" ]]; then
62-
SERVICE_FLAGS[9]="--multiplex"
63-
SERVICE_FLAGS[10]="${MULTIPLEX}"
75+
SERVICE_FLAGS+=("--multiplex")
76+
SERVICE_FLAGS+=("${MULTIPLEX}")
6477

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

100+
if [[ -n "$HEADERS" ]]; then
101+
SERVICE_FLAGS+=("--headers")
102+
SERVICE_FLAGS+=("$HEADERS")
103+
fi
104+
87105
"${DRIVER_DIR}/client" "${SERVICE_FLAGS[@]}" \
88106
--request "${REQUEST_FILE}" \
89107
--response "${RESPONSE_FILE}" \

test/extensions/filters/network/thrift_proxy/integration.cc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,21 @@ void BaseThriftIntegrationTest::preparePayloads(const PayloadOptions& options,
6363
};
6464

6565
if (options.service_name_) {
66+
args.push_back("-s");
6667
args.push_back(*options.service_name_);
6768
}
68-
args.push_back("--");
69+
70+
if (options.headers_.size() > 0) {
71+
args.push_back("-H");
72+
73+
std::vector<std::string> headers;
74+
std::transform(options.headers_.begin(), options.headers_.end(), std::back_inserter(headers),
75+
[](const std::pair<std::string, std::string>& header) -> std::string {
76+
return header.first + "=" + header.second;
77+
});
78+
args.push_back(StringUtil::join(headers, ","));
79+
}
80+
6981
args.push_back(options.method_name_);
7082
std::copy(options.method_args_.begin(), options.method_args_.end(), std::back_inserter(args));
7183

test/extensions/filters/network/thrift_proxy/integration.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ enum class DriverMode {
2929
struct PayloadOptions {
3030
PayloadOptions(TransportType transport, ProtocolType protocol, DriverMode mode,
3131
absl::optional<std::string> service_name, std::string method_name,
32-
std::vector<std::string> method_args = {})
32+
std::vector<std::string> method_args = {},
33+
std::vector<std::pair<std::string, std::string>> headers = {})
3334
: transport_(transport), protocol_(protocol), mode_(mode), service_name_(service_name),
34-
method_name_(method_name), method_args_(method_args) {}
35+
method_name_(method_name), method_args_(method_args), headers_(headers) {}
3536

3637
std::string modeName() const;
3738
std::string transportName() const;
@@ -43,6 +44,7 @@ struct PayloadOptions {
4344
const absl::optional<std::string> service_name_;
4445
const std::string method_name_;
4546
const std::vector<std::string> method_args_;
47+
const std::vector<std::pair<std::string, std::string>> headers_;
4648
};
4749

4850
class BaseThriftIntegrationTest : public BaseIntegrationTest {

test/extensions/filters/network/thrift_proxy/integration_test.cc

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,29 @@ class ThriftConnManagerIntegrationTest
3434
cluster: "cluster_0"
3535
- match:
3636
method_name: "execute"
37+
headers:
38+
- name: "x-header-1"
39+
exact_match: "x-value-1"
40+
- name: "x-header-2"
41+
regex_match: "0.[5-9]"
42+
- name: "x-header-3"
43+
range_match:
44+
start: 100
45+
end: 200
46+
- name: "x-header-4"
47+
prefix_match: "user_id:"
48+
- name: "x-header-5"
49+
suffix_match: "asdf"
3750
route:
3851
cluster: "cluster_1"
3952
- match:
40-
method_name: "poke"
53+
method_name: "execute"
4154
route:
4255
cluster: "cluster_2"
56+
- match:
57+
method_name: "poke"
58+
route:
59+
cluster: "cluster_3"
4360
)EOF";
4461
}
4562

@@ -51,7 +68,16 @@ class ThriftConnManagerIntegrationTest
5168
service_name = "svcname";
5269
}
5370

54-
PayloadOptions options(transport_, protocol_, mode, service_name, "execute");
71+
std::vector<std::pair<std::string, std::string>> headers;
72+
if (transport_ == TransportType::Header) {
73+
headers.push_back(std::make_pair("x-header-1", "x-value-1"));
74+
headers.push_back(std::make_pair("x-header-2", "0.6"));
75+
headers.push_back(std::make_pair("x-header-3", "150"));
76+
headers.push_back(std::make_pair("x-header-4", "user_id:10"));
77+
headers.push_back(std::make_pair("x-header-5", "garbage_asdf"));
78+
}
79+
80+
PayloadOptions options(transport_, protocol_, mode, service_name, "execute", {}, headers);
5581
preparePayloads(options, request_bytes_, response_bytes_);
5682
ASSERT(request_bytes_.length() > 0);
5783
ASSERT(response_bytes_.length() > 0);
@@ -76,16 +102,14 @@ class ThriftConnManagerIntegrationTest
76102
// We allocate as many upstreams as there are clusters, with each upstream being allocated
77103
// to clusters in the order they're defined in the bootstrap config.
78104
void initializeCommon() {
79-
setUpstreamCount(3);
105+
setUpstreamCount(4);
80106

81107
config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) {
82-
auto* c1 = bootstrap.mutable_static_resources()->add_clusters();
83-
c1->MergeFrom(bootstrap.static_resources().clusters()[0]);
84-
c1->set_name("cluster_1");
85-
86-
auto* c2 = bootstrap.mutable_static_resources()->add_clusters();
87-
c2->MergeFrom(bootstrap.static_resources().clusters()[0]);
88-
c2->set_name("cluster_2");
108+
for (int i = 1; i < 4; i++) {
109+
auto* c = bootstrap.mutable_static_resources()->add_clusters();
110+
c->MergeFrom(bootstrap.static_resources().clusters()[0]);
111+
c->set_name(fmt::format("cluster_{}", i));
112+
}
89113
});
90114

91115
BaseThriftIntegrationTest::initialize();
@@ -101,11 +125,13 @@ class ThriftConnManagerIntegrationTest
101125
// while oneway's are handled by the "poke" method. All other requests
102126
// are handled by "execute".
103127
FakeUpstream* getExpectedUpstream(bool oneway) {
104-
int upstreamIdx = 1;
128+
int upstreamIdx = 2;
105129
if (multiplexed_) {
106130
upstreamIdx = 0;
107131
} else if (oneway) {
108-
upstreamIdx = 2;
132+
upstreamIdx = 3;
133+
} else if (transport_ == TransportType::Header) {
134+
upstreamIdx = 1;
109135
}
110136

111137
return fake_upstreams_[upstreamIdx].get();

0 commit comments

Comments
 (0)