New Matchers Implementation

- All except for Regex (which will be submitted with RE2 import PR)
reviewable/pr22951/r16
Donna Dionne 4 years ago
parent 09d3634a6b
commit 60b878d3a5
  1. 38
      BUILD
  2. 4
      Makefile
  3. 376
      src/core/ext/filters/client_channel/lb_policy/xds/xds_routing.cc
  4. 335
      src/core/ext/filters/client_channel/xds/xds_api.cc
  5. 66
      src/core/ext/filters/client_channel/xds/xds_api.h
  6. 114
      src/core/ext/filters/client_channel/xds/xds_client.cc
  7. 5
      src/proto/grpc/testing/xds/BUILD
  8. 44
      src/proto/grpc/testing/xds/lds_rds_for_test.proto
  9. 365
      test/cpp/end2end/xds_end2end_test.cc

38
BUILD

@ -1290,6 +1290,24 @@ grpc_cc_library(
],
)
grpc_cc_library(
name = "grpc_xds_api_header",
hdrs = [
"src/core/ext/filters/client_channel/xds/xds_api.h",
"src/core/ext/filters/client_channel/xds/xds_bootstrap.h",
"src/core/ext/filters/client_channel/xds/xds_client_stats.h",
],
external_deps = [
"upb_lib",
"upb_textformat_lib",
],
language = "c++",
deps = [
"envoy_ads_upbdefs",
"grpc_base",
],
)
grpc_cc_library(
name = "grpc_xds_client",
srcs = [
@ -1300,16 +1318,9 @@ grpc_cc_library(
"src/core/ext/filters/client_channel/xds/xds_client_stats.cc",
],
hdrs = [
"src/core/ext/filters/client_channel/xds/xds_api.h",
"src/core/ext/filters/client_channel/xds/xds_bootstrap.h",
"src/core/ext/filters/client_channel/xds/xds_channel.h",
"src/core/ext/filters/client_channel/xds/xds_channel_args.h",
"src/core/ext/filters/client_channel/xds/xds_client.h",
"src/core/ext/filters/client_channel/xds/xds_client_stats.h",
],
external_deps = [
"upb_lib",
"upb_textformat_lib",
],
language = "c++",
deps = [
@ -1317,6 +1328,7 @@ grpc_cc_library(
"envoy_ads_upbdefs",
"grpc_base",
"grpc_client_channel",
"grpc_xds_api_header",
],
)
@ -1330,16 +1342,9 @@ grpc_cc_library(
"src/core/ext/filters/client_channel/xds/xds_client_stats.cc",
],
hdrs = [
"src/core/ext/filters/client_channel/xds/xds_api.h",
"src/core/ext/filters/client_channel/xds/xds_bootstrap.h",
"src/core/ext/filters/client_channel/xds/xds_channel.h",
"src/core/ext/filters/client_channel/xds/xds_channel_args.h",
"src/core/ext/filters/client_channel/xds/xds_client.h",
"src/core/ext/filters/client_channel/xds/xds_client_stats.h",
],
external_deps = [
"upb_lib",
"upb_textformat_lib",
],
language = "c++",
deps = [
@ -1348,6 +1353,7 @@ grpc_cc_library(
"grpc_base",
"grpc_client_channel",
"grpc_secure",
"grpc_xds_api_header",
],
)
@ -1448,10 +1454,14 @@ grpc_cc_library(
srcs = [
"src/core/ext/filters/client_channel/lb_policy/xds/xds_routing.cc",
],
external_deps = [
"absl/strings",
],
language = "c++",
deps = [
"grpc_base",
"grpc_client_channel",
"grpc_xds_api_header",
],
)

@ -2849,12 +2849,12 @@ $(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.pb.cc: protoc_dep_error
$(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.grpc.pb.cc: protoc_dep_error
else
$(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.pb.cc: src/proto/grpc/testing/xds/lds_rds_for_test.proto $(PROTOBUF_DEP) $(PROTOC_PLUGINS) $(GENDIR)/src/proto/grpc/testing/xds/cds_for_test.pb.cc
$(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.pb.cc: src/proto/grpc/testing/xds/lds_rds_for_test.proto $(PROTOBUF_DEP) $(PROTOC_PLUGINS) $(GENDIR)/src/proto/grpc/testing/xds/cds_for_test.pb.cc $(GENDIR)/src/proto/grpc/testing/xds/eds_for_test.pb.cc
$(E) "[PROTOC] Generating protobuf CC file from $<"
$(Q) mkdir -p `dirname $@`
$(Q) $(PROTOC) -Ithird_party/protobuf/src -I. --cpp_out=$(GENDIR) $<
$(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.grpc.pb.cc: src/proto/grpc/testing/xds/lds_rds_for_test.proto $(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.pb.cc $(PROTOBUF_DEP) $(PROTOC_PLUGINS) $(GENDIR)/src/proto/grpc/testing/xds/cds_for_test.pb.cc $(GENDIR)/src/proto/grpc/testing/xds/cds_for_test.grpc.pb.cc
$(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.grpc.pb.cc: src/proto/grpc/testing/xds/lds_rds_for_test.proto $(GENDIR)/src/proto/grpc/testing/xds/lds_rds_for_test.pb.cc $(PROTOBUF_DEP) $(PROTOC_PLUGINS) $(GENDIR)/src/proto/grpc/testing/xds/cds_for_test.pb.cc $(GENDIR)/src/proto/grpc/testing/xds/cds_for_test.grpc.pb.cc $(GENDIR)/src/proto/grpc/testing/xds/eds_for_test.pb.cc $(GENDIR)/src/proto/grpc/testing/xds/eds_for_test.grpc.pb.cc
$(E) "[GRPC] Generating gRPC's protobuf service CC file from $<"
$(Q) mkdir -p `dirname $@`
$(Q) $(PROTOC) -Ithird_party/protobuf/src -I. --grpc_out=$(GENDIR) --plugin=protoc-gen-grpc=$(PROTOC_PLUGINS_DIR)/grpc_cpp_plugin$(EXECUTABLE_SUFFIX) $<

@ -20,6 +20,8 @@
#include <limits.h>
#include <string.h>
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
@ -30,6 +32,7 @@
#include "src/core/ext/filters/client_channel/lb_policy/child_policy_handler.h"
#include "src/core/ext/filters/client_channel/lb_policy_factory.h"
#include "src/core/ext/filters/client_channel/lb_policy_registry.h"
#include "src/core/ext/filters/client_channel/xds/xds_api.h"
#include "src/core/lib/channel/channel_args.h"
#include "src/core/lib/gpr/string.h"
#include "src/core/lib/gprpp/orphanable.h"
@ -50,12 +53,8 @@ constexpr char kXdsRouting[] = "xds_routing_experimental";
// Config for xds_routing LB policy.
class XdsRoutingLbConfig : public LoadBalancingPolicy::Config {
public:
struct Matcher {
std::string service;
std::string method;
};
struct Route {
Matcher matcher;
XdsApi::RdsUpdate::RdsRoute::Matchers matchers;
std::string action;
};
using RouteTable = std::vector<Route>;
@ -109,20 +108,24 @@ class XdsRoutingLb : public LoadBalancingPolicy {
class RoutePicker : public SubchannelPicker {
public:
struct Route {
XdsRoutingLbConfig::Matcher matcher;
const XdsApi::RdsUpdate::RdsRoute::Matchers* matchers;
RefCountedPtr<ChildPickerWrapper> picker;
};
// Maintains an ordered xds route table as provided by RDS response.
using RouteTable = std::vector<Route>;
explicit RoutePicker(RouteTable route_table)
: route_table_(std::move(route_table)) {}
explicit RoutePicker(RouteTable route_table,
RefCountedPtr<XdsRoutingLbConfig> config)
: route_table_(std::move(route_table)), config_(std::move(config)) {}
PickResult Pick(PickArgs args) override;
private:
RouteTable route_table_;
// Take a reference to config so that we can use
// XdsApi::RdsUpdate::RdsRoute::Matchers from it.
RefCountedPtr<XdsRoutingLbConfig> config_;
};
// Each XdsRoutingChild holds a ref to its parent XdsRoutingLb.
@ -214,27 +217,106 @@ class XdsRoutingLb : public LoadBalancingPolicy {
// XdsRoutingLb::RoutePicker
//
XdsRoutingLb::PickResult XdsRoutingLb::RoutePicker::Pick(PickArgs args) {
absl::string_view path;
absl::optional<absl::string_view> GetMetadataValue(
const std::string& key,
LoadBalancingPolicy::MetadataInterface* initial_metadata) {
// TODO(roth): Using const auto& here trigger a warning in a macos or windows
// build:
//*(args.initial_metadata) is returning values not references.
for (const auto p : *(args.initial_metadata)) {
if (p.first == ":path") {
path = p.second;
break;
GPR_DEBUG_ASSERT(initial_metadata != nullptr);
for (const auto p : *(initial_metadata)) {
if (p.first == key) {
return p.second;
}
}
std::vector<absl::string_view> path_elements =
absl::StrSplit(path.substr(1), '/');
for (const Route& route : route_table_) {
if ((path_elements[0] == route.matcher.service &&
(path_elements[1] == route.matcher.method ||
route.matcher.method.empty())) ||
(route.matcher.service.empty() && route.matcher.method.empty())) {
return route.picker->Pick(args);
return absl::nullopt;
}
bool PathMatch(
const absl::string_view& path,
const XdsApi::RdsUpdate::RdsRoute::Matchers::PathMatcher& path_matcher) {
switch (path_matcher.type) {
case XdsApi::RdsUpdate::RdsRoute::Matchers::PathMatcher::PathMatcherType::
PREFIX:
return absl::StartsWith(path, path_matcher.string_matcher);
case XdsApi::RdsUpdate::RdsRoute::Matchers::PathMatcher::PathMatcherType::
PATH:
return path == path_matcher.string_matcher;
default:
return false;
}
}
bool HeaderMatchHelper(
const XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher& header_matcher,
LoadBalancingPolicy::MetadataInterface* initial_metadata) {
auto value = GetMetadataValue(header_matcher.name, initial_metadata);
if (!value.has_value()) {
if (header_matcher.type == XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::PRESENT) {
return !header_matcher.present_match;
} else {
// For all other header matcher types, we need the header value to
// exist to consider matches.
return false;
}
}
switch (header_matcher.type) {
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::EXACT:
return value.value() == header_matcher.string_matcher;
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::RANGE:
int64_t int_value;
if (!absl::SimpleAtoi(value.value(), &int_value)) {
return false;
}
return int_value >= header_matcher.range_start &&
int_value < header_matcher.range_end;
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::PREFIX:
return absl::StartsWith(value.value(), header_matcher.string_matcher);
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::SUFFIX:
return absl::EndsWith(value.value(), header_matcher.string_matcher);
default:
return false;
}
}
bool HeadersMatch(
LoadBalancingPolicy::PickArgs args,
const std::vector<XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher>&
header_matchers) {
for (const auto& header_matcher : header_matchers) {
bool match = HeaderMatchHelper(header_matcher, args.initial_metadata);
if (header_matcher.invert_match) match = !match;
if (!match) return false;
}
return true;
}
bool UnderFraction(const uint32_t fraction_per_million) {
// Generate a random number in [0, 1000000).
const uint32_t random_number = rand() % 1000000;
return random_number < fraction_per_million;
}
XdsRoutingLb::PickResult XdsRoutingLb::RoutePicker::Pick(PickArgs args) {
for (const Route& route : route_table_) {
// Path matching.
auto path = GetMetadataValue(":path", args.initial_metadata);
GPR_DEBUG_ASSERT(path.has_value());
if (!PathMatch(path.value(), route.matchers->path_matcher)) continue;
// Header Matching.
if (!HeadersMatch(args, route.matchers->header_matchers)) continue;
// Match fraction check
if (route.matchers->fraction_per_million.has_value() &&
!UnderFraction(route.matchers->fraction_per_million.value()))
continue;
// Found a match
return route.picker->Pick(args);
}
PickResult result;
result.type = PickResult::PICK_FAILED;
result.error =
@ -358,7 +440,7 @@ void XdsRoutingLb::UpdateStateLocked() {
RoutePicker::RouteTable route_table;
for (const auto& config_route : config_->route_table()) {
RoutePicker::Route route;
route.matcher = config_route.matcher;
route.matchers = &config_route.matchers;
route.picker = actions_[config_route.action]->picker_wrapper();
if (route.picker == nullptr) {
if (GRPC_TRACE_FLAG_ENABLED(grpc_xds_routing_lb_trace)) {
@ -373,7 +455,7 @@ void XdsRoutingLb::UpdateStateLocked() {
}
route_table.push_back(std::move(route));
}
picker = absl::make_unique<RoutePicker>(std::move(route_table));
picker = absl::make_unique<RoutePicker>(std::move(route_table), config_);
break;
}
case GRPC_CHANNEL_CONNECTING:
@ -683,12 +765,6 @@ class XdsRoutingLbFactory : public LoadBalancingPolicyFactory {
GRPC_ERROR_CREATE_FROM_STATIC_STRING("no valid routes configured");
error_list.push_back(error);
}
if (!route_table.back().matcher.service.empty() ||
!route_table.back().matcher.method.empty()) {
grpc_error* error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"default route must not contain service or method");
error_list.push_back(error);
}
if (!actions_to_be_used.empty()) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"some actions were not referenced by any route"));
@ -731,62 +807,230 @@ class XdsRoutingLbFactory : public LoadBalancingPolicyFactory {
return error_list;
}
static std::vector<grpc_error*> ParseMethodName(
const Json& json, XdsRoutingLbConfig::Matcher* route_config) {
static std::vector<grpc_error*> ParseRoute(
const Json& json, const XdsRoutingLbConfig::ActionMap& action_map,
XdsRoutingLbConfig::Route* route,
std::set<std::string /*action_name*/>* actions_to_be_used) {
std::vector<grpc_error*> error_list;
if (json.type() != Json::Type::OBJECT) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"value should be of type object"));
return error_list;
}
// Parse service
auto it = json.object_value().find("service");
// Parse and ensure one and only one path matcher is set: prefix, path, or
// regex.
bool path_matcher_seen = false;
auto it = json.object_value().find("prefix");
if (it != json.object_value().end()) {
if (it->second.type() != Json::Type::STRING) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:service error: should be string"));
"field:prefix error: should be string"));
} else {
route_config->service = it->second.string_value();
path_matcher_seen = true;
route->matchers.path_matcher.type = XdsApi::RdsUpdate::RdsRoute::
Matchers::PathMatcher::PathMatcherType::PREFIX;
route->matchers.path_matcher.string_matcher = it->second.string_value();
}
}
// Parse method
it = json.object_value().find("method");
it = json.object_value().find("path");
if (it != json.object_value().end()) {
if (it->second.type() != Json::Type::STRING) {
if (path_matcher_seen) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:method error: should be string"));
"field:path error: other path matcher already specified"));
} else {
route_config->method = it->second.string_value();
path_matcher_seen = true;
if (it->second.type() != Json::Type::STRING) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:path error: should be string"));
} else {
route->matchers.path_matcher.type = XdsApi::RdsUpdate::RdsRoute::
Matchers::PathMatcher::PathMatcherType::PATH;
route->matchers.path_matcher.string_matcher =
it->second.string_value();
}
}
}
if (route_config->service.empty() && !route_config->method.empty()) {
if (!path_matcher_seen) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"service is empty when method is not"));
"one path matcher: prefix, path, or regex is required"));
}
return error_list;
}
static std::vector<grpc_error*> ParseRoute(
const Json& json, const XdsRoutingLbConfig::ActionMap& action_map,
XdsRoutingLbConfig::Route* route,
std::set<std::string /*action_name*/>* actions_to_be_used) {
std::vector<grpc_error*> error_list;
if (json.type() != Json::Type::OBJECT) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"value should be of type object"));
return error_list;
// Parse Header Matcher: headers.
it = json.object_value().find("headers");
if (it != json.object_value().end()) {
if (it->second.type() != Json::Type::ARRAY) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:headers error: should be array"));
} else {
const Json::Array& array = it->second.array_value();
for (size_t i = 0; i < array.size(); ++i) {
const Json& header_json = array[i];
if (header_json.type() != Json::Type::OBJECT) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"value should be of type object"));
} else {
route->matchers.header_matchers.emplace_back();
XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher&
header_matcher = route->matchers.header_matchers.back();
auto header_it = header_json.object_value().find("name");
if (header_it == header_json.object_value().end()) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:name error:required field missing"));
} else {
if (header_it->second.type() != Json::Type::STRING) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:name error: should be string"));
} else {
header_matcher.name = header_it->second.string_value();
}
}
header_it = header_json.object_value().find("invert_match");
if (header_it != header_json.object_value().end()) {
if (header_it->second.type() == Json::Type::JSON_TRUE) {
header_matcher.invert_match = true;
} else if (header_it->second.type() == Json::Type::JSON_FALSE) {
header_matcher.invert_match = false;
} else {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:present_match error: should be boolean"));
}
}
// Parse and ensure one and only one header matcher is set per
// header matcher.
bool header_matcher_seen = false;
header_it = header_json.object_value().find("exact_match");
if (header_it != header_json.object_value().end()) {
header_matcher_seen = true;
if (header_it->second.type() != Json::Type::STRING) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:exact_match error: should be string"));
} else {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::EXACT;
header_matcher.string_matcher =
header_it->second.string_value();
}
}
header_it = header_json.object_value().find("range_match");
if (header_it != header_json.object_value().end()) {
if (header_matcher_seen) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:range_match error: other header matcher already "
"specified"));
} else {
header_matcher_seen = true;
if (header_it->second.type() != Json::Type::OBJECT) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:range_match error: should be object"));
} else {
auto range_it =
header_it->second.object_value().find("start");
if (range_it != header_it->second.object_value().end()) {
if (range_it->second.type() != Json::Type::NUMBER) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:start error: should be of number"));
} else {
header_matcher.range_start = gpr_parse_nonnegative_int(
range_it->second.string_value().c_str());
}
} else {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:start missing"));
}
range_it = header_it->second.object_value().find("end");
if (range_it != header_it->second.object_value().end()) {
if (range_it->second.type() != Json::Type::NUMBER) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:end error: should be of number"));
} else {
header_matcher.range_end = gpr_parse_nonnegative_int(
range_it->second.string_value().c_str());
}
} else {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:end missing"));
}
if (header_matcher.range_end > header_matcher.range_start) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::
Matchers::HeaderMatcher::HeaderMatcherType::RANGE;
route->matchers.header_matchers.emplace_back(
header_matcher);
}
}
}
}
header_it = header_json.object_value().find("present_match");
if (header_it != header_json.object_value().end()) {
if (header_matcher_seen) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:present_match error: other header matcher already "
"specified"));
} else {
header_matcher_seen = true;
if (header_it->second.type() == Json::Type::JSON_TRUE) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::PRESENT;
header_matcher.present_match = true;
} else if (header_it->second.type() == Json::Type::JSON_FALSE) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::PRESENT;
header_matcher.present_match = false;
} else {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:present_match error: should be boolean"));
}
}
}
header_it = header_json.object_value().find("prefix_match");
if (header_it != header_json.object_value().end()) {
if (header_matcher_seen) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:prefix_match error: other header matcher already "
"specified"));
} else {
header_matcher_seen = true;
if (header_it->second.type() != Json::Type::STRING) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:prefix_match error: should be string"));
} else {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::PREFIX;
header_matcher.string_matcher =
header_it->second.string_value();
}
}
}
header_it = header_json.object_value().find("suffix_match");
if (header_it != header_json.object_value().end()) {
if (header_matcher_seen) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:suffix_match error: other header matcher already "
"specified"));
} else {
header_matcher_seen = true;
if (header_it->second.type() != Json::Type::STRING) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:suffix_match error: should be string"));
} else {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::SUFFIX;
header_matcher.string_matcher =
header_it->second.string_value();
}
}
}
}
}
}
}
// Parse MethodName.
auto it = json.object_value().find("methodName");
if (it == json.object_value().end()) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:methodName error:required field missing"));
} else {
std::vector<grpc_error*> method_name_errors =
ParseMethodName(it->second, &route->matcher);
if (!method_name_errors.empty()) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_VECTOR(
"field:methodName", &method_name_errors));
// Parse Fraction numerator.
it = json.object_value().find("match_fraction");
if (it != json.object_value().end()) {
if (it->second.type() != Json::Type::NUMBER) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:match_fraction error:must be of type number"));
} else {
route->matchers.fraction_per_million =
gpr_parse_nonnegative_int(it->second.string_value().c_str());
}
}
// Parse action.

@ -23,6 +23,7 @@
#include <cstdlib>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
@ -62,7 +63,9 @@
#include "envoy/config/listener/v2/api_listener.upb.h"
#include "envoy/service/load_stats/v2/lrs.upb.h"
#include "envoy/service/load_stats/v2/lrs.upbdefs.h"
#include "envoy/type/matcher/regex.upb.h"
#include "envoy/type/percent.upb.h"
#include "envoy/type/range.upb.h"
#include "google/protobuf/any.upb.h"
#include "google/protobuf/duration.upb.h"
#include "google/protobuf/struct.upb.h"
@ -152,6 +155,84 @@ bool XdsRoutingEnabled() {
} // namespace
std::string XdsApi::RdsUpdate::RdsRoute::Matchers::PathMatcher::ToString()
const {
std::string path_type_string;
switch (type) {
case PathMatcherType::PATH:
path_type_string = "path match";
break;
case PathMatcherType::PREFIX:
path_type_string = "prefix match";
break;
default:
break;
}
return absl::StrFormat("Path %s:/%s/", path_type_string, string_matcher);
}
std::string XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::ToString()
const {
switch (type) {
case HeaderMatcherType::EXACT:
return absl::StrFormat("Header exact match:%s %s:%s",
invert_match ? " not" : "", name, string_matcher);
case HeaderMatcherType::RANGE:
return absl::StrFormat("Header range match:%s %s:[%d, %d)",
invert_match ? " not" : "", name, range_start,
range_end);
case HeaderMatcherType::PRESENT:
return absl::StrFormat("Header present match:%s %s:%s",
invert_match ? " not" : "", name,
present_match ? "true" : "false");
case HeaderMatcherType::PREFIX:
return absl::StrFormat("Header prefix match:%s %s:%s",
invert_match ? " not" : "", name, string_matcher);
case HeaderMatcherType::SUFFIX:
return absl::StrFormat("Header suffix match:%s %s:%s",
invert_match ? " not" : "", name, string_matcher);
default:
return "";
}
}
std::string XdsApi::RdsUpdate::RdsRoute::Matchers::ToString() const {
std::vector<std::string> contents;
contents.push_back(path_matcher.ToString());
for (const auto& header_it : header_matchers) {
contents.push_back(header_it.ToString());
}
if (fraction_per_million.has_value()) {
contents.push_back(absl::StrFormat("Fraction Per Million %d",
fraction_per_million.value()));
}
return absl::StrJoin(contents, "\n");
}
std::string XdsApi::RdsUpdate::RdsRoute::ClusterWeight::ToString() const {
return absl::StrFormat("{cluster=%s, weight=%d}", name, weight);
}
std::string XdsApi::RdsUpdate::RdsRoute::ToString() const {
std::vector<std::string> contents;
contents.push_back(matchers.ToString());
if (!cluster_name.empty()) {
contents.push_back(absl::StrFormat("Cluster name: %s", cluster_name));
}
for (const auto& weighted_it : weighted_clusters) {
contents.push_back(weighted_it.ToString());
}
return absl::StrJoin(contents, "\n");
}
std::string XdsApi::RdsUpdate::ToString() const {
std::vector<std::string> contents;
for (const auto& route_it : routes) {
contents.push_back(route_it.ToString());
}
return absl::StrJoin(contents, ",\n");
}
XdsApi::XdsApi(XdsClient* client, TraceFlag* tracer,
const XdsBootstrap::Node* node)
: client_(client),
@ -465,8 +546,170 @@ MatchType DomainPatternMatchType(const std::string& domain_pattern) {
return INVALID_MATCH;
}
grpc_error* RoutePathMatchParse(const envoy_api_v2_route_RouteMatch* match,
XdsApi::RdsUpdate::RdsRoute* rds_route,
bool* ignore_route) {
if (envoy_api_v2_route_RouteMatch_has_prefix(match)) {
upb_strview prefix = envoy_api_v2_route_RouteMatch_prefix(match);
// Empty prefix "" is accepted.
if (prefix.size > 0) {
// Prefix "/" is accepted.
if (prefix.data[0] != '/') {
// Prefix which does not start with a / will never match anything, so
// ignore this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
}
std::vector<absl::string_view> prefix_elements =
absl::StrSplit(absl::string_view(prefix.data, prefix.size).substr(1),
absl::MaxSplits('/', 2));
if (prefix_elements.size() > 2) {
// Prefix cannot have more than 2 slashes.
*ignore_route = true;
return GRPC_ERROR_NONE;
} else if (prefix_elements.size() == 2 && prefix_elements[0].empty()) {
// Prefix contains empty string between the 2 slashes
*ignore_route = true;
return GRPC_ERROR_NONE;
}
}
rds_route->matchers.path_matcher.type = XdsApi::RdsUpdate::RdsRoute::
Matchers::PathMatcher::PathMatcherType::PREFIX;
rds_route->matchers.path_matcher.string_matcher =
UpbStringToStdString(prefix);
} else if (envoy_api_v2_route_RouteMatch_has_path(match)) {
upb_strview path = envoy_api_v2_route_RouteMatch_path(match);
if (path.size == 0) {
// Path that is empty will never match anything, so ignore this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
}
if (path.data[0] != '/') {
// Path which does not start with a / will never match anything, so
// ignore this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
}
std::vector<absl::string_view> path_elements =
absl::StrSplit(absl::string_view(path.data, path.size).substr(1),
absl::MaxSplits('/', 2));
if (path_elements.size() != 2) {
// Path not in the required format of /service/method will never match
// anything, so ignore this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
} else if (path_elements[0].empty()) {
// Path contains empty service name will never match anything, so ignore
// this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
} else if (path_elements[1].empty()) {
// Path contains empty method name will never match anything, so ignore
// this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
}
rds_route->matchers.path_matcher.type = XdsApi::RdsUpdate::RdsRoute::
Matchers::PathMatcher::PathMatcherType::PATH;
rds_route->matchers.path_matcher.string_matcher =
UpbStringToStdString(path);
} else {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Invalid route path specifier specified.");
}
return GRPC_ERROR_NONE;
}
grpc_error* RouteHeaderMatchersParse(const envoy_api_v2_route_RouteMatch* match,
XdsApi::RdsUpdate::RdsRoute* rds_route) {
size_t size;
const envoy_api_v2_route_HeaderMatcher* const* headers =
envoy_api_v2_route_RouteMatch_headers(match, &size);
for (size_t i = 0; i < size; ++i) {
const envoy_api_v2_route_HeaderMatcher* header = headers[i];
XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher header_matcher;
header_matcher.name =
UpbStringToStdString(envoy_api_v2_route_HeaderMatcher_name(header));
if (envoy_api_v2_route_HeaderMatcher_has_exact_match(header)) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::EXACT;
header_matcher.string_matcher = UpbStringToStdString(
envoy_api_v2_route_HeaderMatcher_exact_match(header));
} else if (envoy_api_v2_route_HeaderMatcher_has_range_match(header)) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::RANGE;
const envoy_type_Int64Range* range_matcher =
envoy_api_v2_route_HeaderMatcher_range_match(header);
header_matcher.range_start = envoy_type_Int64Range_start(range_matcher);
header_matcher.range_end = envoy_type_Int64Range_end(range_matcher);
if (header_matcher.range_end < header_matcher.range_start) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Invalid range header matcher specifier specified: end "
"cannot be smaller than start.");
}
} else if (envoy_api_v2_route_HeaderMatcher_has_present_match(header)) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::PRESENT;
header_matcher.present_match =
envoy_api_v2_route_HeaderMatcher_present_match(header);
} else if (envoy_api_v2_route_HeaderMatcher_has_prefix_match(header)) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::PREFIX;
header_matcher.string_matcher = UpbStringToStdString(
envoy_api_v2_route_HeaderMatcher_prefix_match(header));
} else if (envoy_api_v2_route_HeaderMatcher_has_suffix_match(header)) {
header_matcher.type = XdsApi::RdsUpdate::RdsRoute::Matchers::
HeaderMatcher::HeaderMatcherType::SUFFIX;
header_matcher.string_matcher = UpbStringToStdString(
envoy_api_v2_route_HeaderMatcher_suffix_match(header));
} else {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Invalid route header matcher specified.");
}
header_matcher.invert_match =
envoy_api_v2_route_HeaderMatcher_invert_match(header);
rds_route->matchers.header_matchers.emplace_back(std::move(header_matcher));
}
return GRPC_ERROR_NONE;
}
grpc_error* RouteRuntimeFractionParse(
const envoy_api_v2_route_RouteMatch* match,
XdsApi::RdsUpdate::RdsRoute* rds_route) {
const envoy_api_v2_core_RuntimeFractionalPercent* runtime_fraction =
envoy_api_v2_route_RouteMatch_runtime_fraction(match);
if (runtime_fraction != nullptr) {
const envoy_type_FractionalPercent* fraction =
envoy_api_v2_core_RuntimeFractionalPercent_default_value(
runtime_fraction);
if (fraction != nullptr) {
uint32_t numerator = envoy_type_FractionalPercent_numerator(fraction);
const auto denominator =
static_cast<envoy_type_FractionalPercent_DenominatorType>(
envoy_type_FractionalPercent_denominator(fraction));
// Normalize to million.
switch (denominator) {
case envoy_type_FractionalPercent_HUNDRED:
numerator *= 10000;
break;
case envoy_type_FractionalPercent_TEN_THOUSAND:
numerator *= 100;
break;
case envoy_type_FractionalPercent_MILLION:
break;
default:
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Unknown denominator type");
}
rds_route->matchers.fraction_per_million = numerator;
}
}
return GRPC_ERROR_NONE;
}
grpc_error* RouteActionParse(const envoy_api_v2_route_Route* route,
XdsApi::RdsUpdate::RdsRoute* rds_route) {
XdsApi::RdsUpdate::RdsRoute* rds_route,
bool* ignore_route) {
if (!envoy_api_v2_route_Route_has_route(route)) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"No RouteAction found in route.");
@ -529,8 +772,9 @@ grpc_error* RouteActionParse(const envoy_api_v2_route_Route* route,
"RouteAction weighted_cluster has no valid clusters specified.");
}
} else {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"No cluster or weighted_clusters found in RouteAction.");
// No cluster or weighted_clusters found in RouteAction, ignore this route.
*ignore_route = true;
return GRPC_ERROR_NONE;
}
return GRPC_ERROR_NONE;
}
@ -606,6 +850,8 @@ grpc_error* RouteConfigParse(
const envoy_api_v2_route_RouteMatch* match =
envoy_api_v2_route_Route_match(route);
XdsApi::RdsUpdate::RdsRoute rds_route;
rds_route.matchers.path_matcher.type = XdsApi::RdsUpdate::RdsRoute::
Matchers::PathMatcher::PathMatcherType::PREFIX;
// if xds routing is not enabled, we must be working on the default route;
// in this case, we must have an empty or single slash prefix.
if (!envoy_api_v2_route_RouteMatch_has_prefix(match)) {
@ -618,8 +864,13 @@ grpc_error* RouteConfigParse(
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Default route must have empty prefix.");
}
grpc_error* error = RouteActionParse(route, &rds_route);
bool ignore_route = false;
grpc_error* error = RouteActionParse(route, &rds_route, &ignore_route);
if (error != GRPC_ERROR_NONE) return error;
if (ignore_route) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Default route action is ignored.");
}
rds_update->routes.emplace_back(std::move(rds_route));
return GRPC_ERROR_NONE;
}
@ -628,6 +879,24 @@ grpc_error* RouteConfigParse(
const envoy_api_v2_route_Route* route = routes[i];
const envoy_api_v2_route_RouteMatch* match =
envoy_api_v2_route_Route_match(route);
size_t query_parameters_size;
static_cast<void>(envoy_api_v2_route_RouteMatch_query_parameters(
match, &query_parameters_size));
if (query_parameters_size > 0) {
continue;
}
XdsApi::RdsUpdate::RdsRoute rds_route;
bool ignore_route = false;
grpc_error* error = RoutePathMatchParse(match, &rds_route, &ignore_route);
if (error != GRPC_ERROR_NONE) return error;
if (ignore_route) continue;
error = RouteHeaderMatchersParse(match, &rds_route);
if (error != GRPC_ERROR_NONE) return error;
error = RouteRuntimeFractionParse(match, &rds_route);
if (error != GRPC_ERROR_NONE) return error;
error = RouteActionParse(route, &rds_route, &ignore_route);
if (error != GRPC_ERROR_NONE) return error;
if (ignore_route) continue;
const google_protobuf_BoolValue* case_sensitive =
envoy_api_v2_route_RouteMatch_case_sensitive(match);
if (case_sensitive != nullptr &&
@ -635,64 +904,6 @@ grpc_error* RouteConfigParse(
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"case_sensitive if set must be set to true.");
}
XdsApi::RdsUpdate::RdsRoute rds_route;
if (envoy_api_v2_route_RouteMatch_has_prefix(match)) {
upb_strview prefix = envoy_api_v2_route_RouteMatch_prefix(match);
// Empty prefix "" is accepted.
if (prefix.size > 0) {
// Prefix "/" is accepted.
if (prefix.data[0] != '/') {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Prefix does not start with a /");
}
if (prefix.size > 1) {
std::vector<absl::string_view> prefix_elements = absl::StrSplit(
absl::string_view(prefix.data, prefix.size).substr(1),
absl::MaxSplits('/', 1));
if (prefix_elements.size() != 2) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Prefix not in the required format of /service/");
} else if (!prefix_elements[1].empty()) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Prefix does not end with a /");
} else if (prefix_elements[0].empty()) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Prefix contains empty service name");
}
rds_route.service = std::string(prefix_elements[0]);
}
}
} else if (envoy_api_v2_route_RouteMatch_has_path(match)) {
upb_strview path = envoy_api_v2_route_RouteMatch_path(match);
if (path.size == 0) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Path if set cannot be empty");
}
if (path.data[0] != '/') {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Path does not start with a /");
}
std::vector<absl::string_view> path_elements = absl::StrSplit(
absl::string_view(path.data, path.size).substr(1), '/');
if (path_elements.size() != 2) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Path not in the required format of /service/method");
} else if (path_elements[0].empty()) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Path contains empty service name");
} else if (path_elements[1].empty()) {
return GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Path contains empty method name");
}
rds_route.service = std::string(path_elements[0]);
rds_route.method = std::string(path_elements[1]);
} else {
// Path specifier types will be supported, ignore but not reject until
// they are implemented.
continue;
}
grpc_error* error = RouteActionParse(route, &rds_route);
if (error != GRPC_ERROR_NONE) return error;
rds_update->routes.emplace_back(std::move(rds_route));
}
if (rds_update->routes.empty()) {

@ -48,12 +48,63 @@ class XdsApi {
static const char* kEdsTypeUrl;
struct RdsUpdate {
// TODO(donnadionne): When we can use absl::variant<>, consider using that
// for: PathMatcher, HeaderMatcher, cluster_name and weighted_clusters
struct RdsRoute {
std::string service;
std::string method;
// TODO(donnadionne): When we can use absl::variant<>, consider using that
// here, to enforce the fact that only one of cluster_name and
// weighted_clusters can be set.
// Matchers for this route.
struct Matchers {
struct PathMatcher {
enum class PathMatcherType {
PATH, // path stored in string_matcher field
PREFIX, // prefix stored in string_matcher field
};
PathMatcherType type;
std::string string_matcher;
bool operator==(const PathMatcher& other) const {
return (type == other.type &&
string_matcher == other.string_matcher);
}
std::string ToString() const;
};
struct HeaderMatcher {
enum class HeaderMatcherType {
EXACT, // value stored in string_matcher field
RANGE, // uses range_start and range_end fields
PRESENT, // uses present_match field
PREFIX, // prefix stored in string_matcher field
SUFFIX, // suffix stored in string_matcher field
};
std::string name;
HeaderMatcherType type;
int64_t range_start;
int64_t range_end;
std::string string_matcher;
bool present_match;
// invert_match field may or may not exisit, so initialize it to
// false.
bool invert_match = false;
bool operator==(const HeaderMatcher& other) const {
return (name == other.name && type == other.type &&
range_start == other.range_start &&
range_end == other.range_end &&
string_matcher == other.string_matcher &&
present_match == other.present_match &&
invert_match == other.invert_match);
}
std::string ToString() const;
};
PathMatcher path_matcher;
std::vector<HeaderMatcher> header_matchers;
absl::optional<uint32_t> fraction_per_million;
bool operator==(const Matchers& other) const {
return (path_matcher == other.path_matcher &&
header_matchers == other.header_matchers &&
fraction_per_million == other.fraction_per_million);
}
std::string ToString() const;
};
Matchers matchers;
// Action for this route.
std::string cluster_name;
struct ClusterWeight {
std::string name;
@ -62,14 +113,16 @@ class XdsApi {
bool operator==(const ClusterWeight& other) const {
return (name == other.name && weight == other.weight);
}
std::string ToString() const;
};
std::vector<ClusterWeight> weighted_clusters;
bool operator==(const RdsRoute& other) const {
return (service == other.service && method == other.method &&
return (matchers == other.matchers &&
cluster_name == other.cluster_name &&
weighted_clusters == other.weighted_clusters);
}
std::string ToString() const;
};
std::vector<RdsRoute> routes;
@ -77,6 +130,7 @@ class XdsApi {
bool operator==(const RdsUpdate& other) const {
return routes == other.routes;
}
std::string ToString() const;
};
// TODO(roth): When we can use absl::variant<>, consider using that

@ -894,14 +894,12 @@ void XdsClient::ChannelState::AdsCallState::AcceptLdsUpdate(
? lds_update->route_config_name.c_str()
: "<inlined>"));
if (lds_update->rds_update.has_value()) {
gpr_log(GPR_INFO, " RouteConfiguration contains %" PRIuPTR " routes",
gpr_log(GPR_INFO, "RouteConfiguration contains %" PRIuPTR " routes",
lds_update->rds_update.value().routes.size());
for (const auto& route : lds_update->rds_update.value().routes) {
gpr_log(GPR_INFO,
" route: { service=\"%s\", "
"method=\"%s\" }, cluster=\"%s\" }",
route.service.c_str(), route.method.c_str(),
route.cluster_name.c_str());
for (size_t i = 0; i < lds_update->rds_update.value().routes.size();
++i) {
gpr_log(GPR_INFO, "Route %" PRIuPTR ":\n%s", i,
lds_update->rds_update.value().routes[i].ToString().c_str());
}
}
}
@ -959,12 +957,9 @@ void XdsClient::ChannelState::AdsCallState::AcceptRdsUpdate(
"[xds_client %p] RDS update received; RouteConfiguration contains "
"%" PRIuPTR " routes",
this, rds_update.value().routes.size());
for (const auto& route : rds_update.value().routes) {
gpr_log(GPR_INFO,
" route: { service=\"%s\", "
"method=\"%s\" }, cluster=\"%s\" }",
route.service.c_str(), route.method.c_str(),
route.cluster_name.c_str());
for (size_t i = 0; i < rds_update.value().routes.size(); ++i) {
gpr_log(GPR_INFO, "Route %" PRIuPTR ":\n%s", i,
rds_update.value().routes[i].ToString().c_str());
}
}
auto& rds_state = state_map_[XdsApi::kRdsTypeUrl];
@ -2024,17 +2019,92 @@ std::string CreateServiceConfigActionCluster(const std::string& cluster_name) {
}
std::string CreateServiceConfigRoute(const std::string& action_name,
const std::string& service,
const std::string& method) {
const XdsApi::RdsUpdate::RdsRoute& route) {
std::vector<std::string> headers;
for (const auto& header : route.matchers.header_matchers) {
std::string header_matcher;
switch (header.type) {
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::EXACT:
header_matcher = absl::StrFormat(" \"exact_match\": \"%s\"",
header.string_matcher);
break;
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::RANGE:
header_matcher = absl::StrFormat(
" \"range_match\":{\n"
" \"start\":%d,\n"
" \"end\":%d\n"
" }",
header.range_start, header.range_end);
break;
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::PRESENT:
header_matcher =
absl::StrFormat(" \"present_match\": %s",
header.present_match ? "true" : "false");
break;
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::PREFIX:
header_matcher = absl::StrFormat(
" \"prefix_match\": \"%s\"", header.string_matcher);
break;
case XdsApi::RdsUpdate::RdsRoute::Matchers::HeaderMatcher::
HeaderMatcherType::SUFFIX:
header_matcher = absl::StrFormat(
" \"suffix_match\": \"%s\"", header.string_matcher);
break;
default:
break;
}
std::vector<std::string> header_parts;
header_parts.push_back(
absl::StrFormat(" { \n"
" \"name\": \"%s\",\n",
header.name));
header_parts.push_back(header_matcher);
if (header.invert_match) {
header_parts.push_back(
absl::StrFormat(",\n"
" \"invert_match\": true"));
}
header_parts.push_back(
absl::StrFormat("\n"
" }"));
headers.push_back(absl::StrJoin(header_parts, ""));
}
std::vector<std::string> headers_service_config;
if (!headers.empty()) {
headers_service_config.push_back("\"headers\":[\n");
headers_service_config.push_back(absl::StrJoin(headers, ","));
headers_service_config.push_back(" ],\n");
}
std::string path_match_str;
switch (route.matchers.path_matcher.type) {
case XdsApi::RdsUpdate::RdsRoute::Matchers::PathMatcher::PathMatcherType::
PREFIX:
path_match_str = absl::StrFormat(
"\"prefix\": \"%s\",\n", route.matchers.path_matcher.string_matcher);
break;
case XdsApi::RdsUpdate::RdsRoute::Matchers::PathMatcher::PathMatcherType::
PATH:
path_match_str = absl::StrFormat(
"\"path\": \"%s\",\n", route.matchers.path_matcher.string_matcher);
break;
}
return absl::StrFormat(
" { \n"
" \"methodName\": {\n"
" \"service\": \"%s\",\n"
" \"method\": \"%s\"\n"
" },\n"
" \"action\": \"%s\"\n"
" %s"
" %s"
" %s"
" \"action\": \"%s\"\n"
" }",
service, method, action_name);
path_match_str, absl::StrJoin(headers_service_config, ""),
route.matchers.fraction_per_million.has_value()
? absl::StrFormat("\"match_fraction\":%d,\n",
route.matchers.fraction_per_million.value())
: "",
action_name);
}
// Create the service config for one weighted cluster.
@ -2216,7 +2286,7 @@ grpc_error* XdsClient::CreateServiceConfig(
absl::StrFormat("%s:%s",
route.weighted_clusters.empty() ? "cds" : "weighted",
action_name),
route.service, route.method));
route));
}
std::vector<std::string> config_parts;
config_parts.push_back(

@ -53,7 +53,10 @@ grpc_proto_library(
],
has_services = True,
well_known_protos = True,
deps = ["cds_for_test_proto"],
deps = [
"cds_for_test_proto",
"eds_for_test_proto",
],
)
grpc_proto_library(

@ -28,20 +28,64 @@ package envoy.api.v2;
import "google/protobuf/any.proto";
import "google/protobuf/wrappers.proto";
import "src/proto/grpc/testing/xds/cds_for_test.proto";
import "src/proto/grpc/testing/xds/eds_for_test.proto";
message RegexMatcher {
message GoogleRE2 {
google.protobuf.UInt32Value max_program_size = 1;
}
oneof engine_type {
GoogleRE2 google_re2 = 1;
}
string regex = 2;
}
message Int64Range {
// start of the range (inclusive)
int64 start = 1;
// end of the range (exclusive)
int64 end = 2;
}
message BoolValue {
// The bool value.
bool value = 1;
}
message HeaderMatcher {
string name = 1;
oneof header_match_specifier {
string exact_match = 4;
RegexMatcher safe_regex_match = 11;
Int64Range range_match = 6;
bool present_match = 7;
string prefix_match = 9;
string suffix_match = 10;
}
bool invert_match = 8;
}
message QueryParameterMatcher {
string name = 1;
}
message RuntimeFractionalPercent {
FractionalPercent default_value = 1;
}
message RouteMatch {
oneof path_specifier {
// If specified, the route is a prefix rule meaning that the prefix must
// match the beginning of the *:path* header.
string prefix = 1;
string path = 2;
RegexMatcher safe_regex = 10;
}
BoolValue case_sensitive = 4;
repeated QueryParameterMatcher query_parameters = 7;
RuntimeFractionalPercent runtime_fraction = 9;
repeated HeaderMatcher headers = 6;
}
message WeightedCluster {

@ -1242,6 +1242,7 @@ class XdsEnd2endTest : public ::testing::TestWithParam<TestType> {
int timeout_ms = 1000;
bool wait_for_ready = false;
bool server_fail = false;
std::vector<std::pair<std::string, std::string>> metadata;
RpcOptions() {}
@ -1269,6 +1270,12 @@ class XdsEnd2endTest : public ::testing::TestWithParam<TestType> {
server_fail = rpc_server_fail;
return *this;
}
RpcOptions& set_metadata(
std::vector<std::pair<std::string, std::string>> rpc_metadata) {
metadata = rpc_metadata;
return *this;
}
};
template <typename Stub>
@ -1463,6 +1470,9 @@ class XdsEnd2endTest : public ::testing::TestWithParam<TestType> {
if (local_response) response = new EchoResponse;
EchoRequest request;
ClientContext context;
for (const auto& metadata : rpc_options.metadata) {
context.AddMetadata(metadata.first, metadata.second);
}
context.set_deadline(
grpc_timeout_milliseconds_to_deadline(rpc_options.timeout_ms));
if (rpc_options.wait_for_ready) context.set_wait_for_ready(true);
@ -2417,11 +2427,7 @@ TEST_P(LdsRdsTest, RouteMatchHasCaseSensitiveFalse) {
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
route1->mutable_match()->mutable_case_sensitive()->set_value(false);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
SetRouteConfiguration(0, route_config);
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
@ -2433,48 +2439,46 @@ TEST_P(LdsRdsTest, RouteMatchHasCaseSensitiveFalse) {
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has a prefix
// string with no "/".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNonEmptyNoSlash) {
// Tests that LDS client should ignore route which has query_parameters.
TEST_P(LdsRdsTest, RouteMatchHasQueryParameters) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("grpc.testing.EchoTest1Service");
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
route1->mutable_match()->add_query_parameters();
SetRouteConfiguration(0, route_config);
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Prefix does not start with a /");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has a prefix
// string does not end with "/".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNoEndingSlash) {
// Tests that LDS client should send a ACK if route match has a prefix
// that is either empty or a single slash
TEST_P(LdsRdsTest, RouteMatchHasValidPrefixEmptyOrSingleSlash) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service");
route1->mutable_match()->set_prefix("");
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("/");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
SetRouteConfiguration(0, route_config);
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
CheckRpcSendFailure();
(void)SendRpc();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message,
"Prefix not in the required format of /service/");
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::ACKED);
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has a prefix
// string does not start with "/".
// Tests that LDS client should ignore route which has a path
// prefix string does not start with "/".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNoLeadingSlash) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
@ -2487,31 +2491,31 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNoLeadingSlash) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Prefix does not start with a /");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has a prefix
// string with extra content outside of "/service/".
// Tests that LDS client should ignore route which has a prefix
// string with more than 2 slashes.
TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixExtraContent) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/Echo1");
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/Echo1/");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Prefix does not end with a /");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has a prefix
// Tests that LDS client should ignore route which has a prefix
// string "//".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNoContent) {
TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixDoubleSlash) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
@ -2523,20 +2527,17 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNoContent) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Prefix contains empty service name");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has path
// Tests that LDS client should ignore route which has path
// but it's empty.
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathEmptyPath) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_path("");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
@ -2544,20 +2545,17 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPathEmptyPath) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Path if set cannot be empty");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has path
// Tests that LDS client should ignore route which has path
// string does not start with "/".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathNoLeadingSlash) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_path("grpc.testing.EchoTest1Service/Echo1");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
@ -2565,20 +2563,17 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPathNoLeadingSlash) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Path does not start with a /");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has path
// string that ends with "/".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathEndsWithSlash) {
// Tests that LDS client should ignore route which has path
// string that has too many slashes; for example, ends with "/".
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathTooManySlashes) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_path("/grpc.testing.EchoTest1Service/Echo1/");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
@ -2586,21 +2581,17 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPathEndsWithSlash) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message,
"Path not in the required format of /service/method");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has path
// string that misses "/" between service and method.
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingMiddleSlash) {
// Tests that LDS client should ignore route which has path
// string that has only 1 slash: missing "/" between service and method.
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathOnlyOneSlash) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_path("/grpc.testing.EchoTest1Service.Echo1");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
@ -2608,21 +2599,17 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingMiddleSlash) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message,
"Path not in the required format of /service/method");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has path
// Tests that LDS client should ignore route which has path
// string that is missing service.
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingService) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_path("//Echo1");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
@ -2630,20 +2617,17 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingService) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Path contains empty service name");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client should send a NACK if route match has path
// Tests that LDS client should ignore route which has path
// string that is missing method.
TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingMethod) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
route1->mutable_match()->set_path("/grpc.testing.EchoTest1Service/");
SetRouteConfiguration(0, route_config);
SetNextResolution({});
@ -2651,7 +2635,7 @@ TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingMethod) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message, "Path contains empty method name");
EXPECT_EQ(response_state.error_message, "No valid routes specified.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
@ -2686,8 +2670,7 @@ TEST_P(LdsRdsTest, RouteActionUnsupportedClusterSpecifier) {
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message,
"No cluster or weighted_clusters found in RouteAction.");
EXPECT_EQ(response_state.error_message, "Default route action is ignored.");
}
TEST_P(LdsRdsTest, RouteActionClusterHasEmptyClusterName) {
@ -2800,6 +2783,30 @@ TEST_P(LdsRdsTest, RouteActionWeightedTargetClusterHasNoWeight) {
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
TEST_P(LdsRdsTest, RouteHeaderMatchInvalidRange) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
const char* kNewCluster1Name = "new_cluster_1";
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
auto* header_matcher1 = route1->mutable_match()->add_headers();
header_matcher1->set_name("header1");
header_matcher1->mutable_range_match()->set_start(1001);
header_matcher1->mutable_range_match()->set_end(1000);
route1->mutable_route()->set_cluster(kNewCluster1Name);
SetRouteConfiguration(0, route_config);
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
CheckRpcSendFailure();
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::NACKED);
EXPECT_EQ(response_state.error_message,
"Invalid range header matcher specifier specified: end "
"cannot be smaller than start.");
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
// Tests that LDS client times out when no response received.
TEST_P(LdsRdsTest, Timeout) {
ResetStub(0, "", 500);
@ -3410,6 +3417,228 @@ TEST_P(LdsRdsTest, XdsRoutingWeightedClusterUpdateClusters) {
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
TEST_P(LdsRdsTest, XdsRoutingHeadersMatching) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
const char* kNewCluster1Name = "new_cluster_1";
const size_t kNumEcho1Rpcs = 100;
const size_t kNumEchoRpcs = 5;
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
// Populate new EDS resources.
AdsServiceImpl::EdsResourceArgs args({
{"locality0", GetBackendPorts(0, 1)},
});
AdsServiceImpl::EdsResourceArgs args1({
{"locality0", GetBackendPorts(1, 2)},
});
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args));
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args1, kNewCluster1Name));
// Populate new CDS resources.
Cluster new_cluster1 = balancers_[0]->ads_service()->default_cluster();
new_cluster1.set_name(kNewCluster1Name);
balancers_[0]->ads_service()->SetCdsResource(new_cluster1);
// Populating Route Configurations for LDS.
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
auto* header_matcher1 = route1->mutable_match()->add_headers();
header_matcher1->set_name("header1");
header_matcher1->set_exact_match("POST");
auto* header_matcher3 = route1->mutable_match()->add_headers();
header_matcher3->set_name("header3");
header_matcher3->mutable_range_match()->set_start(1);
header_matcher3->mutable_range_match()->set_end(1000);
auto* header_matcher4 = route1->mutable_match()->add_headers();
header_matcher4->set_name("header4");
header_matcher4->set_present_match(false);
auto* header_matcher5 = route1->mutable_match()->add_headers();
header_matcher5->set_name("header5");
header_matcher5->set_prefix_match("/grpc");
auto* header_matcher6 = route1->mutable_match()->add_headers();
header_matcher6->set_name("header6");
header_matcher6->set_suffix_match(".cc");
header_matcher6->set_invert_match(true);
route1->mutable_route()->set_cluster(kNewCluster1Name);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
SetRouteConfiguration(0, route_config);
std::vector<std::pair<std::string, std::string>> metadata = {
{"header1", "POST"}, {"header2", "blah"},
{"header3", "1"}, {"header5", "/grpc.testing.EchoTest1Service/"},
{"header6", "grpc.java"},
};
const auto header_match_rpc_options = RpcOptions()
.set_rpc_service(SERVICE_ECHO1)
.set_rpc_method(METHOD_ECHO1)
.set_metadata(std::move(metadata));
// Make sure all backends are up.
WaitForAllBackends(0, 1);
WaitForAllBackends(1, 2, true, header_match_rpc_options);
// Send RPCs.
CheckRpcSendOk(kNumEchoRpcs);
CheckRpcSendOk(kNumEcho1Rpcs, header_match_rpc_options);
EXPECT_EQ(kNumEchoRpcs, backends_[0]->backend_service()->request_count());
EXPECT_EQ(0, backends_[0]->backend_service1()->request_count());
EXPECT_EQ(0, backends_[0]->backend_service2()->request_count());
EXPECT_EQ(0, backends_[1]->backend_service()->request_count());
EXPECT_EQ(kNumEcho1Rpcs, backends_[1]->backend_service1()->request_count());
EXPECT_EQ(0, backends_[1]->backend_service2()->request_count());
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::ACKED);
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
TEST_P(LdsRdsTest, XdsRoutingRuntimeFractionMatching) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
const char* kNewCluster1Name = "new_cluster_1";
const size_t kNumRpcs = 1000;
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
// Populate new EDS resources.
AdsServiceImpl::EdsResourceArgs args({
{"locality0", GetBackendPorts(0, 1)},
});
AdsServiceImpl::EdsResourceArgs args1({
{"locality0", GetBackendPorts(1, 2)},
});
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args));
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args1, kNewCluster1Name));
// Populate new CDS resources.
Cluster new_cluster1 = balancers_[0]->ads_service()->default_cluster();
new_cluster1.set_name(kNewCluster1Name);
balancers_[0]->ads_service()->SetCdsResource(new_cluster1);
// Populating Route Configurations for LDS.
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()
->mutable_runtime_fraction()
->mutable_default_value()
->set_numerator(25);
route1->mutable_route()->set_cluster(kNewCluster1Name);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
SetRouteConfiguration(0, route_config);
WaitForAllBackends(0, 2);
CheckRpcSendOk(kNumRpcs);
const int default_backend_count =
backends_[0]->backend_service()->request_count();
const int matched_backend_count =
backends_[1]->backend_service()->request_count();
const double kErrorTolerance = 0.2;
EXPECT_THAT(default_backend_count,
::testing::AllOf(
::testing::Ge(kNumRpcs * 75 / 100 * (1 - kErrorTolerance)),
::testing::Le(kNumRpcs * 75 / 100 * (1 + kErrorTolerance))));
EXPECT_THAT(matched_backend_count,
::testing::AllOf(
::testing::Ge(kNumRpcs * 25 / 100 * (1 - kErrorTolerance)),
::testing::Le(kNumRpcs * 25 / 100 * (1 + kErrorTolerance))));
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::ACKED);
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
TEST_P(LdsRdsTest, XdsRoutingHeadersMatchingUnmatchCases) {
gpr_setenv("GRPC_XDS_EXPERIMENTAL_ROUTING", "true");
const char* kNewCluster1Name = "new_cluster_1";
const char* kNewCluster2Name = "new_cluster_2";
const char* kNewCluster3Name = "new_cluster_3";
const size_t kNumEcho1Rpcs = 100;
const size_t kNumEchoRpcs = 5;
SetNextResolution({});
SetNextResolutionForLbChannelAllBalancers();
// Populate new EDS resources.
AdsServiceImpl::EdsResourceArgs args({
{"locality0", GetBackendPorts(0, 1)},
});
AdsServiceImpl::EdsResourceArgs args1({
{"locality0", GetBackendPorts(1, 2)},
});
AdsServiceImpl::EdsResourceArgs args2({
{"locality0", GetBackendPorts(2, 3)},
});
AdsServiceImpl::EdsResourceArgs args3({
{"locality0", GetBackendPorts(3, 4)},
});
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args));
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args1, kNewCluster1Name));
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args2, kNewCluster2Name));
balancers_[0]->ads_service()->SetEdsResource(
AdsServiceImpl::BuildEdsResource(args3, kNewCluster3Name));
// Populate new CDS resources.
Cluster new_cluster1 = balancers_[0]->ads_service()->default_cluster();
new_cluster1.set_name(kNewCluster1Name);
balancers_[0]->ads_service()->SetCdsResource(new_cluster1);
Cluster new_cluster2 = balancers_[0]->ads_service()->default_cluster();
new_cluster2.set_name(kNewCluster2Name);
balancers_[0]->ads_service()->SetCdsResource(new_cluster2);
Cluster new_cluster3 = balancers_[0]->ads_service()->default_cluster();
new_cluster1.set_name(kNewCluster3Name);
balancers_[0]->ads_service()->SetCdsResource(new_cluster3);
// Populating Route Configurations for LDS.
RouteConfiguration route_config =
balancers_[0]->ads_service()->default_route_config();
auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0);
route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
auto* header_matcher1 = route1->mutable_match()->add_headers();
header_matcher1->set_name("header1");
header_matcher1->set_exact_match("POST");
route1->mutable_route()->set_cluster(kNewCluster1Name);
auto route2 = route_config.mutable_virtual_hosts(0)->add_routes();
route2->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
auto* header_matcher2 = route2->mutable_match()->add_headers();
header_matcher2->set_name("header2");
header_matcher2->mutable_range_match()->set_start(1);
header_matcher2->mutable_range_match()->set_end(1000);
route2->mutable_route()->set_cluster(kNewCluster2Name);
auto route3 = route_config.mutable_virtual_hosts(0)->add_routes();
route3->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/");
auto* header_matcher3 = route3->mutable_match()->add_headers();
header_matcher3->set_name("header3");
header_matcher3->set_suffix_match(".java");
route3->mutable_route()->set_cluster(kNewCluster3Name);
auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes();
default_route->mutable_match()->set_prefix("");
default_route->mutable_route()->set_cluster(kDefaultResourceName);
SetRouteConfiguration(0, route_config);
// Send headers which will mismatch each route
std::vector<std::pair<std::string, std::string>> metadata = {
{"header1", "POST1"},
{"header2", "1000"},
{"header3", "grpc.cpp"},
};
WaitForAllBackends(0, 1);
CheckRpcSendOk(kNumEchoRpcs, RpcOptions().set_metadata(metadata));
CheckRpcSendOk(kNumEcho1Rpcs, RpcOptions()
.set_rpc_service(SERVICE_ECHO1)
.set_rpc_method(METHOD_ECHO1)
.set_metadata(metadata));
// Verify that only the default backend got RPCs since all previous routes
// were mismatched.
for (size_t i = 1; i < 4; ++i) {
EXPECT_EQ(0, backends_[i]->backend_service()->request_count());
EXPECT_EQ(0, backends_[i]->backend_service1()->request_count());
EXPECT_EQ(0, backends_[i]->backend_service2()->request_count());
}
EXPECT_EQ(kNumEchoRpcs, backends_[0]->backend_service()->request_count());
EXPECT_EQ(kNumEcho1Rpcs, backends_[0]->backend_service1()->request_count());
EXPECT_EQ(0, backends_[0]->backend_service2()->request_count());
const auto& response_state = RouteConfigurationResponseState(0);
EXPECT_EQ(response_state.state, AdsServiceImpl::ResponseState::ACKED);
gpr_unsetenv("GRPC_XDS_EXPERIMENTAL_ROUTING");
}
using CdsTest = BasicTest;
// Tests that CDS client should send an ACK upon correct CDS response.

Loading…
Cancel
Save