From c72a67b6daf1ffc878f2f31b3c6ff71d842f0e9c Mon Sep 17 00:00:00 2001 From: "Mark D. Roth" Date: Mon, 14 Nov 2022 15:17:53 -0800 Subject: [PATCH] xDS RouteConfig: use ValidationErrors and add unit test (#31418) Specific changes: - Use absl::variant<> for HashPolicy types. - Pull validation of resulting LB policy configs out of ClusterSpecifierPlugin registry and into RouteConfig validation. - Don't ignore missing Any fields in HTTP filter configs if is_optional is true, since is_optional should apply only to unsupported filter types, not to malformed resource protos. - Use ExtractXdsExtension() for HttpConnectionManager config itself. - Minor API improvements to StatusCodeSet to make it a bit more useful in tests. - Changed StringMatcher to return the underlying error message when a regex fails to compile. - Fix a bug whereby a ClusterSpecifierPlugin would not be returned if it was not used in a vhost, even if it was used in another vhost. --- CMakeLists.txt | 117 + build_autogenerated.yaml | 42 + .../resolver/xds/xds_resolver.cc | 37 +- .../ext/xds/xds_cluster_specifier_plugin.cc | 80 +- .../ext/xds/xds_cluster_specifier_plugin.h | 23 +- src/core/ext/xds/xds_common_types.cc | 7 +- src/core/ext/xds/xds_listener.cc | 33 +- src/core/ext/xds/xds_route_config.cc | 1023 ++++---- src/core/ext/xds/xds_route_config.h | 47 +- src/core/lib/channel/status_util.cc | 18 + src/core/lib/channel/status_util.h | 9 +- src/core/lib/matchers/matchers.cc | 4 +- src/proto/grpc/testing/xds/v3/route.proto | 2 + test/core/security/matchers_test.cc | 8 +- test/core/xds/BUILD | 22 + .../xds/xds_listener_resource_type_test.cc | 31 - .../xds_route_config_resource_type_test.cc | 2167 +++++++++++++++++ test/cpp/end2end/xds/xds_csds_end2end_test.cc | 11 +- test/cpp/end2end/xds/xds_end2end_test.cc | 56 - test/cpp/end2end/xds/xds_rls_end2end_test.cc | 218 -- .../end2end/xds/xds_routing_end2end_test.cc | 736 +----- tools/run_tests/generated/tests.json | 24 + 22 files changed, 3040 insertions(+), 1675 deletions(-) create mode 100644 test/core/xds/xds_route_config_resource_type_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index a00e7add37d..9294749b7ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1289,6 +1289,7 @@ if(gRPC_BUILD_TESTS) if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX) add_dependencies(buildtests_cxx xds_rls_end2end_test) endif() + add_dependencies(buildtests_cxx xds_route_config_resource_type_test) if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX) add_dependencies(buildtests_cxx xds_routing_end2end_test) endif() @@ -23974,6 +23975,122 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX) endif() +endif() +if(gRPC_BUILD_TESTS) + +add_executable(xds_route_config_resource_type_test + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/lookup/v1/rls_config.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/lookup/v1/rls_config.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/lookup/v1/rls_config.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/lookup/v1/rls_config.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/reflection/v1alpha/reflection.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/reflection/v1alpha/reflection.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/reflection/v1alpha/reflection.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/reflection/v1alpha/reflection.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/address.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/address.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/address.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/address.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/base.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/base.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/base.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/base.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/expr.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/expr.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/expr.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/expr.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/extension.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/extension.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/extension.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/extension.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault_common.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault_common.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault_common.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/fault_common.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/http_filter_rbac.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/http_filter_rbac.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/http_filter_rbac.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/http_filter_rbac.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/metadata.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/metadata.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/metadata.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/metadata.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/path.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/path.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/path.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/path.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/percent.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/percent.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/percent.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/percent.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/range.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/range.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/range.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/range.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/rbac.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/rbac.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/rbac.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/rbac.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/regex.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/regex.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/regex.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/regex.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/route.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/route.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/route.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/route.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/string.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/string.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/string.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/string.grpc.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/typed_struct.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/typed_struct.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/typed_struct.pb.h + ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/typed_struct.grpc.pb.h + test/core/xds/xds_route_config_resource_type_test.cc + test/cpp/util/cli_call.cc + test/cpp/util/cli_credentials.cc + test/cpp/util/proto_file_parser.cc + test/cpp/util/proto_reflection_descriptor_database.cc + test/cpp/util/service_describer.cc + third_party/googletest/googletest/src/gtest-all.cc + third_party/googletest/googlemock/src/gmock-all.cc +) + +target_include_directories(xds_route_config_resource_type_test + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR} + ${_gRPC_RE2_INCLUDE_DIR} + ${_gRPC_SSL_INCLUDE_DIR} + ${_gRPC_UPB_GENERATED_DIR} + ${_gRPC_UPB_GRPC_GENERATED_DIR} + ${_gRPC_UPB_INCLUDE_DIR} + ${_gRPC_XXHASH_INCLUDE_DIR} + ${_gRPC_ZLIB_INCLUDE_DIR} + third_party/googletest/googletest/include + third_party/googletest/googletest + third_party/googletest/googlemock/include + third_party/googletest/googlemock + ${_gRPC_PROTO_GENS_DIR} +) + +target_link_libraries(xds_route_config_resource_type_test + ${_gRPC_BASELIB_LIBRARIES} + ${_gRPC_PROTOBUF_LIBRARIES} + ${_gRPC_ZLIB_LIBRARIES} + ${_gRPC_ALLTARGETS_LIBRARIES} + absl::flags + grpc++ + grpc_test_util +) + + endif() if(gRPC_BUILD_TESTS) if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX) diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml index 48c5e82b1f6..f0aa0ea3ecb 100644 --- a/build_autogenerated.yaml +++ b/build_autogenerated.yaml @@ -12606,6 +12606,48 @@ targets: - linux - posix - mac +- name: xds_route_config_resource_type_test + gtest: true + build: test + language: c++ + headers: + - test/core/util/scoped_env_var.h + - test/cpp/util/cli_call.h + - test/cpp/util/cli_credentials.h + - test/cpp/util/config_grpc_cli.h + - test/cpp/util/proto_file_parser.h + - test/cpp/util/proto_reflection_descriptor_database.h + - test/cpp/util/service_describer.h + src: + - src/proto/grpc/lookup/v1/rls_config.proto + - src/proto/grpc/reflection/v1alpha/reflection.proto + - src/proto/grpc/testing/xds/v3/address.proto + - src/proto/grpc/testing/xds/v3/base.proto + - src/proto/grpc/testing/xds/v3/expr.proto + - src/proto/grpc/testing/xds/v3/extension.proto + - src/proto/grpc/testing/xds/v3/fault.proto + - src/proto/grpc/testing/xds/v3/fault_common.proto + - src/proto/grpc/testing/xds/v3/http_filter_rbac.proto + - src/proto/grpc/testing/xds/v3/metadata.proto + - src/proto/grpc/testing/xds/v3/path.proto + - src/proto/grpc/testing/xds/v3/percent.proto + - src/proto/grpc/testing/xds/v3/range.proto + - src/proto/grpc/testing/xds/v3/rbac.proto + - src/proto/grpc/testing/xds/v3/regex.proto + - src/proto/grpc/testing/xds/v3/route.proto + - src/proto/grpc/testing/xds/v3/string.proto + - src/proto/grpc/testing/xds/v3/typed_struct.proto + - test/core/xds/xds_route_config_resource_type_test.cc + - test/cpp/util/cli_call.cc + - test/cpp/util/cli_credentials.cc + - test/cpp/util/proto_file_parser.cc + - test/cpp/util/proto_reflection_descriptor_database.cc + - test/cpp/util/service_describer.cc + deps: + - absl/flags:flag + - grpc++ + - grpc_test_util + uses_polling: false - name: xds_routing_end2end_test gtest: true build: test diff --git a/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc b/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc index 27ecf833c1e..e6ddd072d20 100644 --- a/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc +++ b/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc @@ -638,23 +638,21 @@ void XdsResolver::XdsConfigSelector::MaybeAddCluster(const std::string& name) { } absl::optional HeaderHashHelper( - const XdsRouteConfigResource::Route::RouteAction::HashPolicy& policy, + const XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header& + header_policy, grpc_metadata_batch* initial_metadata) { - GPR_ASSERT(policy.type == - XdsRouteConfigResource::Route::RouteAction::HashPolicy::HEADER); std::string value_buffer; absl::optional header_value = XdsRouting::GetHeaderValue( - initial_metadata, policy.header_name, &value_buffer); - if (!header_value.has_value()) { - return absl::nullopt; - } - if (policy.regex != nullptr) { + initial_metadata, header_policy.header_name, &value_buffer); + if (!header_value.has_value()) return absl::nullopt; + if (header_policy.regex != nullptr) { // If GetHeaderValue() did not already store the value in // value_buffer, copy it there now, so we can modify it. if (header_value->data() != value_buffer.data()) { value_buffer = std::string(*header_value); } - RE2::GlobalReplace(&value_buffer, *policy.regex, policy.regex_substitution); + RE2::GlobalReplace(&value_buffer, *header_policy.regex, + header_policy.regex_substitution); header_value = value_buffer; } return XXH64(header_value->data(), header_value->size(), 0); @@ -732,17 +730,16 @@ XdsResolver::XdsConfigSelector::GetCallConfig(GetCallConfigArgs args) { // Generate a hash. absl::optional hash; for (const auto& hash_policy : route_action->hash_policies) { - absl::optional new_hash; - switch (hash_policy.type) { - case XdsRouteConfigResource::Route::RouteAction::HashPolicy::HEADER: - new_hash = HeaderHashHelper(hash_policy, args.initial_metadata); - break; - case XdsRouteConfigResource::Route::RouteAction::HashPolicy::CHANNEL_ID: - new_hash = resolver_->channel_id(); - break; - default: - GPR_ASSERT(0); - } + absl::optional new_hash = Match( + hash_policy.policy, + [&](const XdsRouteConfigResource::Route::RouteAction::HashPolicy:: + Header& header) { + return HeaderHashHelper(header, args.initial_metadata); + }, + [&](const XdsRouteConfigResource::Route::RouteAction::HashPolicy:: + ChannelId&) -> absl::optional { + return resolver_->channel_id(); + }); if (new_hash.has_value()) { // Rotating the old value prevents duplicate hash rules from cancelling // each other out and preserves all of the entropy diff --git a/src/core/ext/xds/xds_cluster_specifier_plugin.cc b/src/core/ext/xds/xds_cluster_specifier_plugin.cc index e68cb7601f1..cb267020cae 100644 --- a/src/core/ext/xds/xds_cluster_specifier_plugin.cc +++ b/src/core/ext/xds/xds_cluster_specifier_plugin.cc @@ -20,11 +20,10 @@ #include -#include #include #include -#include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/types/variant.h" #include "upb/json_encode.h" @@ -33,10 +32,7 @@ #include -#include "src/core/lib/config/core_configuration.h" -#include "src/core/lib/gprpp/ref_counted_ptr.h" #include "src/core/lib/json/json.h" -#include "src/core/lib/load_balancing/lb_policy_registry.h" #include "src/proto/grpc/lookup/v1/rls_config.upb.h" #include "src/proto/grpc/lookup/v1/rls_config.upbdefs.h" @@ -46,34 +42,38 @@ namespace grpc_core { // XdsRouteLookupClusterSpecifierPlugin // -const char* kXdsRouteLookupClusterSpecifierPluginConfigName = - "grpc.lookup.v1.RouteLookupClusterSpecifier"; +absl::string_view XdsRouteLookupClusterSpecifierPlugin::ConfigProtoName() + const { + return "grpc.lookup.v1.RouteLookupClusterSpecifier"; +} void XdsRouteLookupClusterSpecifierPlugin::PopulateSymtab( upb_DefPool* symtab) const { grpc_lookup_v1_RouteLookupConfig_getmsgdef(symtab); } -absl::StatusOr -XdsRouteLookupClusterSpecifierPlugin::GenerateLoadBalancingPolicyConfig( - XdsExtension extension, upb_Arena* arena, upb_DefPool* symtab) const { +Json XdsRouteLookupClusterSpecifierPlugin::GenerateLoadBalancingPolicyConfig( + XdsExtension extension, upb_Arena* arena, upb_DefPool* symtab, + ValidationErrors* errors) const { absl::string_view* serialized_plugin_config = absl::get_if(&extension.value); if (serialized_plugin_config == nullptr) { - return absl::InvalidArgumentError("could not parse plugin config"); + errors->AddError("could not parse plugin config"); + return {}; } const auto* specifier = grpc_lookup_v1_RouteLookupClusterSpecifier_parse( serialized_plugin_config->data(), serialized_plugin_config->size(), arena); if (specifier == nullptr) { - return absl::InvalidArgumentError("Could not parse plugin config"); + errors->AddError("could not parse plugin config"); + return {}; } const auto* plugin_config = grpc_lookup_v1_RouteLookupClusterSpecifier_route_lookup_config(specifier); if (plugin_config == nullptr) { - return absl::InvalidArgumentError( - "Could not get route lookup config from route lookup cluster " - "specifier"); + ValidationErrors::ScopedField field(errors, ".route_lookup_config"); + errors->AddError("field not present"); + return {}; } upb::Status status; const upb_MessageDef* msg_type = @@ -81,42 +81,23 @@ XdsRouteLookupClusterSpecifierPlugin::GenerateLoadBalancingPolicyConfig( size_t json_size = upb_JsonEncode(plugin_config, msg_type, symtab, 0, nullptr, 0, status.ptr()); if (json_size == static_cast(-1)) { - return absl::InvalidArgumentError( - absl::StrCat("failed to dump proto to JSON: ", - upb_Status_ErrorMessage(status.ptr()))); + errors->AddError(absl::StrCat("failed to dump proto to JSON: ", + upb_Status_ErrorMessage(status.ptr()))); + return {}; } void* buf = upb_Arena_Malloc(arena, json_size + 1); upb_JsonEncode(plugin_config, msg_type, symtab, 0, reinterpret_cast(buf), json_size + 1, status.ptr()); - Json::Object rls_policy; auto json = Json::Parse(reinterpret_cast(buf)); GPR_ASSERT(json.ok()); - rls_policy["routeLookupConfig"] = std::move(*json); - Json::Object cds_policy; - cds_policy["cds_experimental"] = Json::Object(); - Json::Array child_policy; - child_policy.emplace_back(std::move(cds_policy)); - rls_policy["childPolicy"] = std::move(child_policy); - rls_policy["childPolicyConfigTargetFieldName"] = "cluster"; - Json::Object policy; - policy["rls_experimental"] = std::move(rls_policy); - Json::Array policies; - policies.emplace_back(std::move(policy)); - Json lb_policy_config(std::move(policies)); - // TODO(roth): If/when we ever add a second plugin, refactor this code - // somehow such that we automatically validate the resulting config against - // the gRPC LB policy registry instead of requiring each plugin to do that - // itself. - auto config = - CoreConfiguration::Get().lb_policy_registry().ParseLoadBalancingConfig( - lb_policy_config); - if (!config.ok()) { - return absl::InvalidArgumentError(absl::StrCat( - kXdsRouteLookupClusterSpecifierPluginConfigName, - " ClusterSpecifierPlugin returned invalid LB policy config: ", - config.status().message())); - } - return lb_policy_config.Dump(); + return Json::Array{Json::Object{ + {"rls_experimental", + Json::Object{ + {"routeLookupConfig", std::move(*json)}, + {"childPolicy", + Json::Array{Json::Object{{"cds_experimental", Json::Object()}}}}, + {"childPolicyConfigTargetFieldName", "cluster"}, + }}}}; } // @@ -124,14 +105,13 @@ XdsRouteLookupClusterSpecifierPlugin::GenerateLoadBalancingPolicyConfig( // XdsClusterSpecifierPluginRegistry::XdsClusterSpecifierPluginRegistry() { - RegisterPlugin(std::make_unique(), - kXdsRouteLookupClusterSpecifierPluginConfigName); + RegisterPlugin(std::make_unique()); } void XdsClusterSpecifierPluginRegistry::RegisterPlugin( - std::unique_ptr plugin, - absl::string_view config_proto_type_name) { - registry_[config_proto_type_name] = std::move(plugin); + std::unique_ptr plugin) { + absl::string_view name = plugin->ConfigProtoName(); + registry_[name] = std::move(plugin); } const XdsClusterSpecifierPluginImpl* diff --git a/src/core/ext/xds/xds_cluster_specifier_plugin.h b/src/core/ext/xds/xds_cluster_specifier_plugin.h index 3e98d9d1a9f..b1803081383 100644 --- a/src/core/ext/xds/xds_cluster_specifier_plugin.h +++ b/src/core/ext/xds/xds_cluster_specifier_plugin.h @@ -21,15 +21,15 @@ #include #include -#include #include -#include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "upb/arena.h" #include "upb/def.h" #include "src/core/ext/xds/xds_common_types.h" +#include "src/core/lib/gprpp/validation_errors.h" +#include "src/core/lib/json/json.h" namespace grpc_core { @@ -37,21 +37,27 @@ class XdsClusterSpecifierPluginImpl { public: virtual ~XdsClusterSpecifierPluginImpl() = default; + // Returns the config proto message name. + virtual absl::string_view ConfigProtoName() const = 0; + // Loads the proto message into the upb symtab. virtual void PopulateSymtab(upb_DefPool* symtab) const = 0; // Returns the LB policy config in JSON form. - virtual absl::StatusOr GenerateLoadBalancingPolicyConfig( - XdsExtension extension, upb_Arena* arena, upb_DefPool* symtab) const = 0; + virtual Json GenerateLoadBalancingPolicyConfig( + XdsExtension extension, upb_Arena* arena, upb_DefPool* symtab, + ValidationErrors* errors) const = 0; }; class XdsRouteLookupClusterSpecifierPlugin : public XdsClusterSpecifierPluginImpl { + absl::string_view ConfigProtoName() const override; + void PopulateSymtab(upb_DefPool* symtab) const override; - absl::StatusOr GenerateLoadBalancingPolicyConfig( - XdsExtension extension, upb_Arena* arena, - upb_DefPool* symtab) const override; + Json GenerateLoadBalancingPolicyConfig( + XdsExtension extension, upb_Arena* arena, upb_DefPool* symtab, + ValidationErrors* errors) const override; }; class XdsClusterSpecifierPluginRegistry { @@ -74,8 +80,7 @@ class XdsClusterSpecifierPluginRegistry { return *this; } - void RegisterPlugin(std::unique_ptr plugin, - absl::string_view config_proto_type_name); + void RegisterPlugin(std::unique_ptr plugin); void PopulateSymtab(upb_DefPool* symtab) const; diff --git a/src/core/ext/xds/xds_common_types.cc b/src/core/ext/xds/xds_common_types.cc index 8ddbdb7420b..ccf717aa610 100644 --- a/src/core/ext/xds/xds_common_types.cc +++ b/src/core/ext/xds/xds_common_types.cc @@ -454,7 +454,7 @@ absl::optional ExtractXdsExtension( ValidationErrors::ScopedField field(errors, ".type_url"); if (extension.type.empty()) { errors->AddError("field not present"); - return; + return false; } size_t pos = extension.type.rfind('/'); if (pos == absl::string_view::npos || pos == extension.type.size() - 1) { @@ -462,9 +462,10 @@ absl::optional ExtractXdsExtension( } else { extension.type = extension.type.substr(pos + 1); } + return true; }; extension.type = UpbStringToAbsl(google_protobuf_Any_type_url(any)); - strip_type_prefix(); + if (!strip_type_prefix()) return absl::nullopt; extension.validation_fields.emplace_back( errors, absl::StrCat(".value[", extension.type, "]")); absl::string_view any_value = UpbStringToAbsl(google_protobuf_Any_value(any)); @@ -478,7 +479,7 @@ absl::optional ExtractXdsExtension( } extension.type = UpbStringToAbsl(xds_type_v3_TypedStruct_type_url(typed_struct)); - strip_type_prefix(); + if (!strip_type_prefix()) return absl::nullopt; extension.validation_fields.emplace_back( errors, absl::StrCat(".value[", extension.type, "]")); auto* protobuf_struct = xds_type_v3_TypedStruct_value(typed_struct); diff --git a/src/core/ext/xds/xds_listener.cc b/src/core/ext/xds/xds_listener.cc index dfe14cca9de..febeeacbef6 100644 --- a/src/core/ext/xds/xds_listener.cc +++ b/src/core/ext/xds/xds_listener.cc @@ -395,15 +395,10 @@ XdsListenerResource::HttpConnectionManager HttpConnectionManagerParse( const google_protobuf_Any* typed_config = envoy_extensions_filters_network_http_connection_manager_v3_HttpFilter_typed_config( http_filter); - if (typed_config == nullptr) { - if (!is_optional) errors->AddError("field not present"); - continue; - } auto extension = ExtractXdsExtension(context, typed_config, errors); - const XdsHttpFilterImpl* filter_impl = nullptr; - if (extension.has_value()) { - filter_impl = http_filter_registry.GetFilterForType(extension->type); - } + if (!extension.has_value()) continue; + const XdsHttpFilterImpl* filter_impl = + http_filter_registry.GetFilterForType(extension->type); if (filter_impl == nullptr) { if (!is_optional) errors->AddError("unsupported filter type"); continue; @@ -463,13 +458,9 @@ XdsListenerResource::HttpConnectionManager HttpConnectionManagerParse( const envoy_config_route_v3_RouteConfiguration* route_config = envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_route_config( http_connection_manager_proto); - auto rds_update = XdsRouteConfigResource::Parse(context, route_config); - if (!rds_update.ok()) { - ValidationErrors::ScopedField field(errors, ".route_config"); - errors->AddError(rds_update.status().message()); - } else { - http_connection_manager.route_config = std::move(*rds_update); - } + ValidationErrors::ScopedField field(errors, ".route_config"); + http_connection_manager.route_config = + XdsRouteConfigResource::Parse(context, route_config, errors); } else { // Validate that RDS must be used to get the route_config dynamically. const envoy_extensions_filters_network_http_connection_manager_v3_Rds* rds = @@ -506,14 +497,10 @@ absl::StatusOr LdsResourceParseClient( ValidationErrors::ScopedField field(&errors, "api_listener.api_listener"); auto* api_listener_field = envoy_config_listener_v3_ApiListener_api_listener(api_listener); - if (api_listener_field == nullptr) { - errors.AddError("field not present"); - } else { - auto extension = ExtractXdsExtension(context, api_listener_field, &errors); - if (extension.has_value()) { - lds_update.listener = HttpConnectionManagerParse( - /*is_client=*/true, context, std::move(*extension), &errors); - } + auto extension = ExtractXdsExtension(context, api_listener_field, &errors); + if (extension.has_value()) { + lds_update.listener = HttpConnectionManagerParse( + /*is_client=*/true, context, std::move(*extension), &errors); } if (!errors.ok()) return errors.status("errors validating ApiListener"); return std::move(lds_update); diff --git a/src/core/ext/xds/xds_route_config.cc b/src/core/ext/xds/xds_route_config.cc index 280fbf470aa..c7d35cfc1d4 100644 --- a/src/core/ext/xds/xds_route_config.cc +++ b/src/core/ext/xds/xds_route_config.cc @@ -63,12 +63,14 @@ #include "src/core/ext/xds/xds_resource_type.h" #include "src/core/ext/xds/xds_routing.h" #include "src/core/lib/channel/status_util.h" +#include "src/core/lib/config/core_configuration.h" #include "src/core/lib/debug/trace.h" #include "src/core/lib/gpr/string.h" #include "src/core/lib/gprpp/env.h" #include "src/core/lib/gprpp/match.h" #include "src/core/lib/gprpp/time.h" -#include "src/core/lib/gprpp/validation_errors.h" +#include "src/core/lib/json/json.h" +#include "src/core/lib/load_balancing/lb_policy_registry.h" #include "src/core/lib/matchers/matchers.h" namespace grpc_core { @@ -122,13 +124,12 @@ std::string XdsRouteConfigResource::Route::Matchers::ToString() const { } // -// XdsRouteConfigResource::Route::RouteAction::HashPolicy +// XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header // -XdsRouteConfigResource::Route::RouteAction::HashPolicy::HashPolicy( - const HashPolicy& other) - : type(other.type), - header_name(other.header_name), +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header::Header( + const Header& other) + : header_name(other.header_name), regex_substitution(other.regex_substitution) { if (other.regex != nullptr) { regex = @@ -136,10 +137,9 @@ XdsRouteConfigResource::Route::RouteAction::HashPolicy::HashPolicy( } } -XdsRouteConfigResource::Route::RouteAction::HashPolicy& -XdsRouteConfigResource::Route::RouteAction::HashPolicy::operator=( - const HashPolicy& other) { - type = other.type; +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header& +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header::operator=( + const Header& other) { header_name = other.header_name; if (other.regex != nullptr) { regex = @@ -149,58 +149,52 @@ XdsRouteConfigResource::Route::RouteAction::HashPolicy::operator=( return *this; } -XdsRouteConfigResource::Route::RouteAction::HashPolicy::HashPolicy( - HashPolicy&& other) noexcept - : type(other.type), - header_name(std::move(other.header_name)), +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header::Header( + Header&& other) noexcept + : header_name(std::move(other.header_name)), regex(std::move(other.regex)), regex_substitution(std::move(other.regex_substitution)) {} -XdsRouteConfigResource::Route::RouteAction::HashPolicy& -XdsRouteConfigResource::Route::RouteAction::HashPolicy::operator=( - HashPolicy&& other) noexcept { - type = other.type; +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header& +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header::operator=( + Header&& other) noexcept { header_name = std::move(other.header_name); regex = std::move(other.regex); regex_substitution = std::move(other.regex_substitution); return *this; } -bool XdsRouteConfigResource::Route::RouteAction::HashPolicy::HashPolicy:: -operator==(const HashPolicy& other) const { - if (type != other.type) return false; - if (type == Type::HEADER) { - if (regex == nullptr) { - if (other.regex != nullptr) return false; - } else { - if (other.regex == nullptr) return false; - return header_name == other.header_name && - regex->pattern() == other.regex->pattern() && - regex_substitution == other.regex_substitution; - } +bool XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header::operator==( + const Header& other) const { + if (header_name != other.header_name) return false; + if (regex == nullptr) { + if (other.regex != nullptr) return false; + } else { + if (other.regex == nullptr) return false; + if (regex->pattern() != other.regex->pattern()) return false; } - return true; + return regex_substitution == other.regex_substitution; +} + +std::string +XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header::ToString() + const { + return absl::StrCat("Header ", header_name, "/", + (regex == nullptr) ? "" : regex->pattern(), "/", + regex_substitution); } +// +// XdsRouteConfigResource::Route::RouteAction::HashPolicy +// + std::string XdsRouteConfigResource::Route::RouteAction::HashPolicy::ToString() const { - std::vector contents; - switch (type) { - case Type::HEADER: - contents.push_back("type=HEADER"); - break; - case Type::CHANNEL_ID: - contents.push_back("type=CHANNEL_ID"); - break; - } - contents.push_back( - absl::StrFormat("terminal=%s", terminal ? "true" : "false")); - if (type == Type::HEADER) { - contents.push_back(absl::StrFormat( - "Header %s:/%s/%s", header_name, - (regex == nullptr) ? "" : regex->pattern(), regex_substitution)); - } - return absl::StrCat("{", absl::StrJoin(contents, ", "), "}"); + std::string type = Match( + policy, [](const Header& header) { return header.ToString(); }, + [](const ChannelId&) -> std::string { return "ChannelId"; }); + return absl::StrCat("{", type, ", terminal=", terminal ? "true" : "false", + "}"); } // @@ -328,10 +322,10 @@ std::string XdsRouteConfigResource::ToString() const { namespace { -absl::StatusOr -ClusterSpecifierPluginParse( +XdsRouteConfigResource::ClusterSpecifierPluginMap ClusterSpecifierPluginParse( const XdsResourceType::DecodeContext& context, - const envoy_config_route_v3_RouteConfiguration* route_config) { + const envoy_config_route_v3_RouteConfiguration* route_config, + ValidationErrors* errors) { XdsRouteConfigResource::ClusterSpecifierPluginMap cluster_specifier_plugin_map; const auto& cluster_specifier_plugin_registry = @@ -343,6 +337,10 @@ ClusterSpecifierPluginParse( envoy_config_route_v3_RouteConfiguration_cluster_specifier_plugins( route_config, &num_cluster_specifier_plugins); for (size_t i = 0; i < num_cluster_specifier_plugins; ++i) { + bool is_optional = envoy_config_route_v3_ClusterSpecifierPlugin_is_optional( + cluster_specifier_plugin[i]); + ValidationErrors::ScopedField field( + errors, absl::StrCat(".cluster_specifier_plugins[", i, "].extension")); const envoy_config_core_v3_TypedExtensionConfig* typed_extension_config = envoy_config_route_v3_ClusterSpecifierPlugin_extension( cluster_specifier_plugin[i]); @@ -350,54 +348,58 @@ ClusterSpecifierPluginParse( envoy_config_core_v3_TypedExtensionConfig_name(typed_extension_config)); if (cluster_specifier_plugin_map.find(name) != cluster_specifier_plugin_map.end()) { - return absl::InvalidArgumentError(absl::StrCat( - "Duplicated definition of cluster_specifier_plugin ", name)); + ValidationErrors::ScopedField field(errors, ".name"); + errors->AddError(absl::StrCat("duplicate name \"", name, "\"")); + } else { + // Add a sentinel entry in case we encounter an error later, just so we + // don't generate duplicate errors for each route that uses this plugin. + cluster_specifier_plugin_map[name] = ""; } + ValidationErrors::ScopedField field2(errors, ".typed_config"); const google_protobuf_Any* any = envoy_config_core_v3_TypedExtensionConfig_typed_config( typed_extension_config); - if (any == nullptr) { - return absl::InvalidArgumentError( - "Could not obtrain TypedExtensionConfig for plugin config."); - } - ValidationErrors validation_errors; - ValidationErrors::ScopedField field( - &validation_errors, absl::StrCat(".cluster_specifier_plugins[", i, - "].extension.typed_config")); - auto extension = ExtractXdsExtension(context, any, &validation_errors); - if (!validation_errors.ok()) { - return validation_errors.status("could not determine extension type"); - } - GPR_ASSERT(extension.has_value()); - bool is_optional = envoy_config_route_v3_ClusterSpecifierPlugin_is_optional( - cluster_specifier_plugin[i]); + auto extension = ExtractXdsExtension(context, any, errors); + if (!extension.has_value()) continue; const XdsClusterSpecifierPluginImpl* cluster_specifier_plugin_impl = cluster_specifier_plugin_registry.GetPluginForType(extension->type); - std::string lb_policy_config; if (cluster_specifier_plugin_impl == nullptr) { - if (!is_optional) { - return absl::InvalidArgumentError(absl::StrCat( - "Unknown ClusterSpecifierPlugin type ", extension->type)); + if (is_optional) { + // Empty string indicates an optional plugin. + // This is used later when validating routes, and since we will skip + // any routes that refer to this plugin, we won't wind up including + // this plugin in the resource that we return to the watcher. + cluster_specifier_plugin_map[std::move(name)] = ""; + } else { + // Not optional, report error. + errors->AddError("unsupported ClusterSpecifierPlugin type"); } - // Optional plugin, leave lb_policy_config empty. + continue; + } + const size_t original_error_size = errors->size(); + Json lb_policy_config = + cluster_specifier_plugin_impl->GenerateLoadBalancingPolicyConfig( + std::move(*extension), context.arena, context.symtab, errors); + if (errors->size() != original_error_size) continue; + auto config = + CoreConfiguration::Get().lb_policy_registry().ParseLoadBalancingConfig( + lb_policy_config); + if (!config.ok()) { + errors->AddError(absl::StrCat( + "ClusterSpecifierPlugin returned invalid LB policy config: ", + config.status().message())); } else { - auto config = - cluster_specifier_plugin_impl->GenerateLoadBalancingPolicyConfig( - std::move(*extension), context.arena, context.symtab); - if (!config.ok()) return config.status(); - lb_policy_config = std::move(*config); + cluster_specifier_plugin_map[std::move(name)] = lb_policy_config.Dump(); } - cluster_specifier_plugin_map[std::move(name)] = std::move(lb_policy_config); } return cluster_specifier_plugin_map; } -absl::Status RoutePathMatchParse(const envoy_config_route_v3_RouteMatch* match, - XdsRouteConfigResource::Route* route, - bool* ignore_route) { +absl::optional RoutePathMatchParse( + const envoy_config_route_v3_RouteMatch* match, ValidationErrors* errors) { + bool case_sensitive = true; auto* case_sensitive_ptr = envoy_config_route_v3_RouteMatch_case_sensitive(match); - bool case_sensitive = true; if (case_sensitive_ptr != nullptr) { case_sensitive = google_protobuf_BoolValue_value(case_sensitive_ptr); } @@ -406,25 +408,18 @@ absl::Status RoutePathMatchParse(const envoy_config_route_v3_RouteMatch* match, if (envoy_config_route_v3_RouteMatch_has_prefix(match)) { absl::string_view prefix = UpbStringToAbsl(envoy_config_route_v3_RouteMatch_prefix(match)); - // Empty prefix "" is accepted. + // For any prefix that cannot match a path of the form "/service/method", + // ignore the route. if (!prefix.empty()) { - // Prefix "/" is accepted. - if (prefix[0] != '/') { - // Prefix which does not start with a / will never match anything, so - // ignore this route. - *ignore_route = true; - return absl::OkStatus(); - } + // Does not start with a slash. + if (prefix[0] != '/') return absl::nullopt; std::vector prefix_elements = absl::StrSplit(prefix.substr(1), absl::MaxSplits('/', 2)); - if (prefix_elements.size() > 2) { - // Prefix cannot have more than 2 slashes. - *ignore_route = true; - return absl::OkStatus(); - } else if (prefix_elements.size() == 2 && prefix_elements[0].empty()) { - // Prefix contains empty string between the 2 slashes - *ignore_route = true; - return absl::OkStatus(); + // More than 2 slashes. + if (prefix_elements.size() > 2) return absl::nullopt; + // Two consecutive slashes. + if (prefix_elements.size() == 2 && prefix_elements[0].empty()) { + return absl::nullopt; } } type = StringMatcher::Type::kPrefix; @@ -432,35 +427,19 @@ absl::Status RoutePathMatchParse(const envoy_config_route_v3_RouteMatch* match, } else if (envoy_config_route_v3_RouteMatch_has_path(match)) { absl::string_view path = UpbStringToAbsl(envoy_config_route_v3_RouteMatch_path(match)); - if (path.empty()) { - // Path that is empty will never match anything, so ignore this route. - *ignore_route = true; - return absl::OkStatus(); - } - if (path[0] != '/') { - // Path which does not start with a / will never match anything, so - // ignore this route. - *ignore_route = true; - return absl::OkStatus(); - } + // For any path not of the form "/service/method", ignore the route. + // Empty path. + if (path.empty()) return absl::nullopt; + // Does not start with a slash. + if (path[0] != '/') return absl::nullopt; std::vector path_elements = absl::StrSplit(path.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 absl::OkStatus(); - } else if (path_elements[0].empty()) { - // Path contains empty service name will never match anything, so ignore - // this route. - *ignore_route = true; - return absl::OkStatus(); - } else if (path_elements[1].empty()) { - // Path contains empty method name will never match anything, so ignore - // this route. - *ignore_route = true; - return absl::OkStatus(); - } + // Number of slashes does not equal 2. + if (path_elements.size() != 2) return absl::nullopt; + // Empty service name. + if (path_elements[0].empty()) return absl::nullopt; + // Empty method name. + if (path_elements[1].empty()) return absl::nullopt; type = StringMatcher::Type::kExact; match_string = std::string(path); } else if (envoy_config_route_v3_RouteMatch_has_safe_regex(match)) { @@ -471,27 +450,30 @@ absl::Status RoutePathMatchParse(const envoy_config_route_v3_RouteMatch* match, match_string = UpbStringToStdString( envoy_type_matcher_v3_RegexMatcher_regex(regex_matcher)); } else { - return absl::InvalidArgumentError( - "Invalid route path specifier specified."); + errors->AddError("invalid path specifier"); + return absl::nullopt; } absl::StatusOr string_matcher = StringMatcher::Create(type, match_string, case_sensitive); if (!string_matcher.ok()) { - return absl::InvalidArgumentError( - absl::StrCat("path matcher: ", string_matcher.status().message())); + errors->AddError(absl::StrCat("error creating path matcher: ", + string_matcher.status().message())); + return absl::nullopt; } - route->matchers.path_matcher = std::move(string_matcher.value()); - return absl::OkStatus(); + return std::move(*string_matcher); } -absl::Status RouteHeaderMatchersParse( - const envoy_config_route_v3_RouteMatch* match, - XdsRouteConfigResource::Route* route) { +void RouteHeaderMatchersParse(const envoy_config_route_v3_RouteMatch* match, + XdsRouteConfigResource::Route* route, + ValidationErrors* errors) { size_t size; const envoy_config_route_v3_HeaderMatcher* const* headers = envoy_config_route_v3_RouteMatch_headers(match, &size); for (size_t i = 0; i < size; ++i) { + ValidationErrors::ScopedField field(errors, + absl::StrCat(".headers[", i, "]")); const envoy_config_route_v3_HeaderMatcher* header = headers[i]; + GPR_ASSERT(header != nullptr); const std::string name = UpbStringToStdString(envoy_config_route_v3_HeaderMatcher_name(header)); HeaderMatcher::Type type; @@ -503,6 +485,18 @@ absl::Status RouteHeaderMatchersParse( type = HeaderMatcher::Type::kExact; match_string = UpbStringToStdString( envoy_config_route_v3_HeaderMatcher_exact_match(header)); + } else if (envoy_config_route_v3_HeaderMatcher_has_prefix_match(header)) { + type = HeaderMatcher::Type::kPrefix; + match_string = UpbStringToStdString( + envoy_config_route_v3_HeaderMatcher_prefix_match(header)); + } else if (envoy_config_route_v3_HeaderMatcher_has_suffix_match(header)) { + type = HeaderMatcher::Type::kSuffix; + match_string = UpbStringToStdString( + envoy_config_route_v3_HeaderMatcher_suffix_match(header)); + } else if (envoy_config_route_v3_HeaderMatcher_has_contains_match(header)) { + type = HeaderMatcher::Type::kContains; + match_string = UpbStringToStdString( + envoy_config_route_v3_HeaderMatcher_contains_match(header)); } else if (envoy_config_route_v3_HeaderMatcher_has_safe_regex_match( header)) { const envoy_type_matcher_v3_RegexMatcher* regex_matcher = @@ -520,21 +514,9 @@ absl::Status RouteHeaderMatchersParse( } else if (envoy_config_route_v3_HeaderMatcher_has_present_match(header)) { type = HeaderMatcher::Type::kPresent; present_match = envoy_config_route_v3_HeaderMatcher_present_match(header); - } else if (envoy_config_route_v3_HeaderMatcher_has_prefix_match(header)) { - type = HeaderMatcher::Type::kPrefix; - match_string = UpbStringToStdString( - envoy_config_route_v3_HeaderMatcher_prefix_match(header)); - } else if (envoy_config_route_v3_HeaderMatcher_has_suffix_match(header)) { - type = HeaderMatcher::Type::kSuffix; - match_string = UpbStringToStdString( - envoy_config_route_v3_HeaderMatcher_suffix_match(header)); - } else if (envoy_config_route_v3_HeaderMatcher_has_contains_match(header)) { - type = HeaderMatcher::Type::kContains; - match_string = UpbStringToStdString( - envoy_config_route_v3_HeaderMatcher_contains_match(header)); } else { - return absl::InvalidArgumentError( - "Invalid route header matcher specified."); + errors->AddError("invalid header matcher"); + continue; } bool invert_match = envoy_config_route_v3_HeaderMatcher_invert_match(header); @@ -542,18 +524,17 @@ absl::Status RouteHeaderMatchersParse( HeaderMatcher::Create(name, type, match_string, range_start, range_end, present_match, invert_match); if (!header_matcher.ok()) { - return absl::InvalidArgumentError( - absl::StrCat("header matcher: ", header_matcher.status().message())); + errors->AddError(absl::StrCat("cannot create header matcher: ", + header_matcher.status().message())); + } else { + route->matchers.header_matchers.emplace_back(std::move(*header_matcher)); } - route->matchers.header_matchers.emplace_back( - std::move(header_matcher.value())); } - return absl::OkStatus(); } -absl::Status RouteRuntimeFractionParse( - const envoy_config_route_v3_RouteMatch* match, - XdsRouteConfigResource::Route* route) { +void RouteRuntimeFractionParse(const envoy_config_route_v3_RouteMatch* match, + XdsRouteConfigResource::Route* route, + ValidationErrors* errors) { const envoy_config_core_v3_RuntimeFractionalPercent* runtime_fraction = envoy_config_route_v3_RouteMatch_runtime_fraction(match); if (runtime_fraction != nullptr) { @@ -562,9 +543,8 @@ absl::Status RouteRuntimeFractionParse( runtime_fraction); if (fraction != nullptr) { uint32_t numerator = envoy_type_v3_FractionalPercent_numerator(fraction); - const auto denominator = - static_cast( - envoy_type_v3_FractionalPercent_denominator(fraction)); + const uint32_t denominator = + envoy_type_v3_FractionalPercent_denominator(fraction); // Normalize to million. switch (denominator) { case envoy_type_v3_FractionalPercent_HUNDRED: @@ -575,108 +555,98 @@ absl::Status RouteRuntimeFractionParse( break; case envoy_type_v3_FractionalPercent_MILLION: break; - default: - return absl::InvalidArgumentError("Unknown denominator type"); + default: { + ValidationErrors::ScopedField field( + errors, ".runtime_fraction.default_value.denominator"); + errors->AddError("unknown denominator type"); + return; + } } route->matchers.fraction_per_million = numerator; } } - return absl::OkStatus(); } template -absl::StatusOr -ParseTypedPerFilterConfig( +XdsRouteConfigResource::TypedPerFilterConfig ParseTypedPerFilterConfig( const XdsResourceType::DecodeContext& context, const ParentType* parent, const EntryType* (*entry_func)(const ParentType*, size_t*), upb_StringView (*key_func)(const EntryType*), - const google_protobuf_Any* (*value_func)(const EntryType*)) { + const google_protobuf_Any* (*value_func)(const EntryType*), + ValidationErrors* errors) { XdsRouteConfigResource::TypedPerFilterConfig typed_per_filter_config; size_t filter_it = kUpb_Map_Begin; while (true) { const auto* filter_entry = entry_func(parent, &filter_it); if (filter_entry == nullptr) break; absl::string_view key = UpbStringToAbsl(key_func(filter_entry)); - if (key.empty()) { - return absl::InvalidArgumentError("empty filter name in map"); - } + ValidationErrors::ScopedField field(errors, absl::StrCat("[", key, "]")); + if (key.empty()) errors->AddError("filter name must be non-empty"); const google_protobuf_Any* any = value_func(filter_entry); - GPR_ASSERT(any != nullptr); - absl::string_view filter_type = - UpbStringToAbsl(google_protobuf_Any_type_url(any)); - if (filter_type.empty()) { - return absl::InvalidArgumentError( - absl::StrCat("no filter config specified for filter name ", key)); - } + auto extension = ExtractXdsExtension(context, any, errors); + if (!extension.has_value()) continue; + auto* extension_to_use = &*extension; + absl::optional nested_extension; bool is_optional = false; - if (filter_type == - "type.googleapis.com/envoy.config.route.v3.FilterConfig") { - upb_StringView any_value = google_protobuf_Any_value(any); + if (extension->type == "envoy.config.route.v3.FilterConfig") { + absl::string_view* serialized_config = + absl::get_if(&extension->value); + if (serialized_config == nullptr) { + errors->AddError("could not parse FilterConfig"); + continue; + } const auto* filter_config = envoy_config_route_v3_FilterConfig_parse( - any_value.data, any_value.size, context.arena); + serialized_config->data(), serialized_config->size(), context.arena); if (filter_config == nullptr) { - return absl::InvalidArgumentError( - absl::StrCat("could not parse FilterConfig wrapper for ", key)); + errors->AddError("could not parse FilterConfig"); + continue; } is_optional = envoy_config_route_v3_FilterConfig_is_optional(filter_config); any = envoy_config_route_v3_FilterConfig_config(filter_config); - if (any == nullptr) { - if (is_optional) continue; - return absl::InvalidArgumentError( - absl::StrCat("no filter config specified for filter name ", key)); - } + extension->validation_fields.emplace_back(errors, ".config"); + nested_extension = ExtractXdsExtension(context, any, errors); + if (!nested_extension.has_value()) continue; + extension_to_use = &*nested_extension; } - ValidationErrors errors; - ValidationErrors::ScopedField field( - &errors, absl::StrCat(".typed_per_filter_config[", key, "]")); - auto extension = ExtractXdsExtension(context, any, &errors); - if (!errors.ok()) { - return errors.status("could not determine extension type"); - } - GPR_ASSERT(extension.has_value()); const auto& http_filter_registry = static_cast(context.client->bootstrap()) .http_filter_registry(); const XdsHttpFilterImpl* filter_impl = - http_filter_registry.GetFilterForType(extension->type); + http_filter_registry.GetFilterForType(extension_to_use->type); if (filter_impl == nullptr) { - if (is_optional) continue; - return absl::InvalidArgumentError(absl::StrCat( - "no filter registered for config type ", extension->type)); + if (!is_optional) errors->AddError("unsupported filter type"); + continue; } absl::optional filter_config = - filter_impl->GenerateFilterConfigOverride(std::move(*extension), - context.arena, &errors); - if (!errors.ok()) { - return errors.status("errors validation extension"); + filter_impl->GenerateFilterConfigOverride(std::move(*extension_to_use), + context.arena, errors); + if (filter_config.has_value()) { + typed_per_filter_config[std::string(key)] = std::move(*filter_config); } - GPR_ASSERT(filter_config.has_value()); - typed_per_filter_config[std::string(key)] = std::move(*filter_config); } return typed_per_filter_config; } -absl::Status RetryPolicyParse( +XdsRouteConfigResource::RetryPolicy RetryPolicyParse( const XdsResourceType::DecodeContext& context, - const envoy_config_route_v3_RetryPolicy* retry_policy, - absl::optional* retry) { - std::vector errors; - XdsRouteConfigResource::RetryPolicy retry_to_return; + const envoy_config_route_v3_RetryPolicy* retry_policy_proto, + ValidationErrors* errors) { + XdsRouteConfigResource::RetryPolicy retry_policy; auto retry_on = UpbStringToStdString( - envoy_config_route_v3_RetryPolicy_retry_on(retry_policy)); + envoy_config_route_v3_RetryPolicy_retry_on(retry_policy_proto)); std::vector codes = absl::StrSplit(retry_on, ','); for (const auto& code : codes) { if (code == "cancelled") { - retry_to_return.retry_on.Add(GRPC_STATUS_CANCELLED); + retry_policy.retry_on.Add(GRPC_STATUS_CANCELLED); } else if (code == "deadline-exceeded") { - retry_to_return.retry_on.Add(GRPC_STATUS_DEADLINE_EXCEEDED); + retry_policy.retry_on.Add(GRPC_STATUS_DEADLINE_EXCEEDED); } else if (code == "internal") { - retry_to_return.retry_on.Add(GRPC_STATUS_INTERNAL); + retry_policy.retry_on.Add(GRPC_STATUS_INTERNAL); } else if (code == "resource-exhausted") { - retry_to_return.retry_on.Add(GRPC_STATUS_RESOURCE_EXHAUSTED); + retry_policy.retry_on.Add(GRPC_STATUS_RESOURCE_EXHAUSTED); } else if (code == "unavailable") { - retry_to_return.retry_on.Add(GRPC_STATUS_UNAVAILABLE); + retry_policy.retry_on.Add(GRPC_STATUS_UNAVAILABLE); } else { if (GRPC_TRACE_FLAG_ENABLED(*context.tracer)) { gpr_log(GPR_INFO, "Unsupported retry_on policy %s.", @@ -685,198 +655,90 @@ absl::Status RetryPolicyParse( } } const google_protobuf_UInt32Value* num_retries = - envoy_config_route_v3_RetryPolicy_num_retries(retry_policy); + envoy_config_route_v3_RetryPolicy_num_retries(retry_policy_proto); if (num_retries != nullptr) { uint32_t num_retries_value = google_protobuf_UInt32Value_value(num_retries); if (num_retries_value == 0) { - errors.emplace_back( - "RouteAction RetryPolicy num_retries set to invalid value 0."); + ValidationErrors::ScopedField field(errors, ".num_retries"); + errors->AddError("must be greater than 0"); } else { - retry_to_return.num_retries = num_retries_value; + retry_policy.num_retries = num_retries_value; } } else { - retry_to_return.num_retries = 1; + retry_policy.num_retries = 1; } const envoy_config_route_v3_RetryPolicy_RetryBackOff* backoff = - envoy_config_route_v3_RetryPolicy_retry_back_off(retry_policy); + envoy_config_route_v3_RetryPolicy_retry_back_off(retry_policy_proto); if (backoff != nullptr) { - const google_protobuf_Duration* base_interval = - envoy_config_route_v3_RetryPolicy_RetryBackOff_base_interval(backoff); - if (base_interval == nullptr) { - errors.emplace_back( - "RouteAction RetryPolicy RetryBackoff missing base interval."); - } else { - ValidationErrors validation_errors; - retry_to_return.retry_back_off.base_interval = - ParseDuration(base_interval, &validation_errors); - if (!validation_errors.ok()) { - errors.emplace_back( - validation_errors.status("base_interval").message()); + ValidationErrors::ScopedField field(errors, ".retry_back_off"); + { + ValidationErrors::ScopedField field(errors, ".base_interval"); + const google_protobuf_Duration* base_interval = + envoy_config_route_v3_RetryPolicy_RetryBackOff_base_interval(backoff); + if (base_interval == nullptr) { + errors->AddError("field not present"); + } else { + retry_policy.retry_back_off.base_interval = + ParseDuration(base_interval, errors); } } - const google_protobuf_Duration* max_interval = - envoy_config_route_v3_RetryPolicy_RetryBackOff_max_interval(backoff); - Duration max; - if (max_interval != nullptr) { - ValidationErrors validation_errors; - max = ParseDuration(max_interval, &validation_errors); - if (!validation_errors.ok()) { - errors.emplace_back(validation_errors.status("max_interval").message()); + { + ValidationErrors::ScopedField field(errors, ".max_interval"); + const google_protobuf_Duration* max_interval = + envoy_config_route_v3_RetryPolicy_RetryBackOff_max_interval(backoff); + Duration max; + if (max_interval != nullptr) { + max = ParseDuration(max_interval, errors); + } else { + // if max interval is not set, it is 10x the base. + max = 10 * retry_policy.retry_back_off.base_interval; } - } else { - // if max interval is not set, it is 10x the base. - max = 10 * retry_to_return.retry_back_off.base_interval; + retry_policy.retry_back_off.max_interval = max; } - retry_to_return.retry_back_off.max_interval = max; } else { - retry_to_return.retry_back_off.base_interval = Duration::Milliseconds(25); - retry_to_return.retry_back_off.max_interval = Duration::Milliseconds(250); - } - // Return result. - if (!errors.empty()) { - return absl::InvalidArgumentError(absl::StrCat( - "Errors parsing retry policy: [", absl::StrJoin(errors, "; "), "]")); + retry_policy.retry_back_off.base_interval = Duration::Milliseconds(25); + retry_policy.retry_back_off.max_interval = Duration::Milliseconds(250); } - *retry = retry_to_return; - return absl::OkStatus(); + return retry_policy; } -absl::StatusOr RouteActionParse( +absl::optional RouteActionParse( const XdsResourceType::DecodeContext& context, - const envoy_config_route_v3_Route* route_msg, + const envoy_config_route_v3_RouteAction* route_action_proto, const std::map& cluster_specifier_plugin_map, - bool* ignore_route) { - XdsRouteConfigResource::Route::RouteAction route; - const envoy_config_route_v3_RouteAction* route_action = - envoy_config_route_v3_Route_route(route_msg); - // Get the cluster or weighted_clusters in the RouteAction. - if (envoy_config_route_v3_RouteAction_has_cluster(route_action)) { - std::string cluster_name = UpbStringToStdString( - envoy_config_route_v3_RouteAction_cluster(route_action)); - if (cluster_name.empty()) { - return absl::InvalidArgumentError( - "RouteAction cluster contains empty cluster name."); - } - route.action = XdsRouteConfigResource::Route::RouteAction::ClusterName{ - std::move(cluster_name)}; - } else if (envoy_config_route_v3_RouteAction_has_weighted_clusters( - route_action)) { - std::vector - action_weighted_clusters; - const envoy_config_route_v3_WeightedCluster* weighted_cluster = - envoy_config_route_v3_RouteAction_weighted_clusters(route_action); - uint32_t total_weight = 100; - const google_protobuf_UInt32Value* weight = - envoy_config_route_v3_WeightedCluster_total_weight(weighted_cluster); - if (weight != nullptr) { - total_weight = google_protobuf_UInt32Value_value(weight); - } - size_t clusters_size; - const envoy_config_route_v3_WeightedCluster_ClusterWeight* const* clusters = - envoy_config_route_v3_WeightedCluster_clusters(weighted_cluster, - &clusters_size); - uint32_t sum_of_weights = 0; - for (size_t j = 0; j < clusters_size; ++j) { - const envoy_config_route_v3_WeightedCluster_ClusterWeight* - cluster_weight = clusters[j]; - XdsRouteConfigResource::Route::RouteAction::ClusterWeight cluster; - cluster.name = UpbStringToStdString( - envoy_config_route_v3_WeightedCluster_ClusterWeight_name( - cluster_weight)); - if (cluster.name.empty()) { - return absl::InvalidArgumentError( - "RouteAction weighted_cluster cluster contains empty cluster " - "name."); - } - const google_protobuf_UInt32Value* weight = - envoy_config_route_v3_WeightedCluster_ClusterWeight_weight( - cluster_weight); - if (weight == nullptr) { - return absl::InvalidArgumentError( - "RouteAction weighted_cluster cluster missing weight"); - } - cluster.weight = google_protobuf_UInt32Value_value(weight); - if (cluster.weight == 0) continue; - sum_of_weights += cluster.weight; - auto typed_per_filter_config = ParseTypedPerFilterConfig< - envoy_config_route_v3_WeightedCluster_ClusterWeight, - envoy_config_route_v3_WeightedCluster_ClusterWeight_TypedPerFilterConfigEntry>( - context, cluster_weight, - envoy_config_route_v3_WeightedCluster_ClusterWeight_typed_per_filter_config_next, - envoy_config_route_v3_WeightedCluster_ClusterWeight_TypedPerFilterConfigEntry_key, - envoy_config_route_v3_WeightedCluster_ClusterWeight_TypedPerFilterConfigEntry_value); - if (!typed_per_filter_config.ok()) { - return typed_per_filter_config.status(); - } - cluster.typed_per_filter_config = std::move(*typed_per_filter_config); - action_weighted_clusters.emplace_back(std::move(cluster)); - } - if (total_weight != sum_of_weights) { - return absl::InvalidArgumentError( - "RouteAction weighted_cluster has incorrect total weight"); - } - if (action_weighted_clusters.empty()) { - return absl::InvalidArgumentError( - "RouteAction weighted_cluster has no valid clusters specified."); - } - route.action = std::move(action_weighted_clusters); - } else if (XdsRlsEnabled() && - envoy_config_route_v3_RouteAction_has_cluster_specifier_plugin( - route_action)) { - std::string plugin_name = UpbStringToStdString( - envoy_config_route_v3_RouteAction_cluster_specifier_plugin( - route_action)); - if (plugin_name.empty()) { - return absl::InvalidArgumentError( - "RouteAction cluster contains empty cluster specifier plugin name."); - } - auto it = cluster_specifier_plugin_map.find(plugin_name); - if (it == cluster_specifier_plugin_map.end()) { - return absl::InvalidArgumentError( - absl::StrCat("RouteAction cluster contains cluster specifier plugin " - "name not configured: ", - plugin_name)); - } - if (it->second.empty()) *ignore_route = true; - route.action = - XdsRouteConfigResource::Route::RouteAction::ClusterSpecifierPluginName{ - std::move(plugin_name)}; - } else { - // No cluster or weighted_clusters or plugin found in RouteAction, ignore - // this route. - *ignore_route = true; - } - if (!*ignore_route) { - const envoy_config_route_v3_RouteAction_MaxStreamDuration* - max_stream_duration = - envoy_config_route_v3_RouteAction_max_stream_duration(route_action); - if (max_stream_duration != nullptr) { - const google_protobuf_Duration* duration = - envoy_config_route_v3_RouteAction_MaxStreamDuration_grpc_timeout_header_max( + ValidationErrors* errors) { + XdsRouteConfigResource::Route::RouteAction route_action; + // grpc_timeout_header_max or max_stream_duration + const auto* max_stream_duration = + envoy_config_route_v3_RouteAction_max_stream_duration(route_action_proto); + if (max_stream_duration != nullptr) { + ValidationErrors::ScopedField field(errors, ".max_stream_duration"); + const google_protobuf_Duration* duration = + envoy_config_route_v3_RouteAction_MaxStreamDuration_grpc_timeout_header_max( + max_stream_duration); + if (duration != nullptr) { + ValidationErrors::ScopedField field(errors, ".grpc_timeout_header_max"); + route_action.max_stream_duration = ParseDuration(duration, errors); + } else { + duration = + envoy_config_route_v3_RouteAction_MaxStreamDuration_max_stream_duration( max_stream_duration); - if (duration == nullptr) { - duration = - envoy_config_route_v3_RouteAction_MaxStreamDuration_max_stream_duration( - max_stream_duration); - } if (duration != nullptr) { - ValidationErrors validation_errors; - route.max_stream_duration = ParseDuration(duration, &validation_errors); - if (!validation_errors.ok()) { - return validation_errors.status("max_stream_duration"); - } + ValidationErrors::ScopedField field(errors, ".max_stream_duration"); + route_action.max_stream_duration = ParseDuration(duration, errors); } } } - // Get HashPolicy from RouteAction + // hash_policy size_t size = 0; const envoy_config_route_v3_RouteAction_HashPolicy* const* hash_policies = - envoy_config_route_v3_RouteAction_hash_policy(route_action, &size); + envoy_config_route_v3_RouteAction_hash_policy(route_action_proto, &size); for (size_t i = 0; i < size; ++i) { - const envoy_config_route_v3_RouteAction_HashPolicy* hash_policy = - hash_policies[i]; + ValidationErrors::ScopedField field(errors, + absl::StrCat(".hash_policy[", i, "]")); + const auto* hash_policy = hash_policies[i]; XdsRouteConfigResource::Route::RouteAction::HashPolicy policy; policy.terminal = envoy_config_route_v3_RouteAction_HashPolicy_terminal(hash_policy); @@ -885,100 +747,260 @@ absl::StatusOr RouteActionParse( filter_state; if ((header = envoy_config_route_v3_RouteAction_HashPolicy_header( hash_policy)) != nullptr) { - policy.type = - XdsRouteConfigResource::Route::RouteAction::HashPolicy::Type::HEADER; - policy.header_name = UpbStringToStdString( + // header + ValidationErrors::ScopedField field(errors, ".header"); + XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header + header_policy; + header_policy.header_name = UpbStringToStdString( envoy_config_route_v3_RouteAction_HashPolicy_Header_header_name( header)); - const struct envoy_type_matcher_v3_RegexMatchAndSubstitute* - regex_rewrite = - envoy_config_route_v3_RouteAction_HashPolicy_Header_regex_rewrite( - header); + if (header_policy.header_name.empty()) { + ValidationErrors::ScopedField field(errors, ".header_name"); + errors->AddError("must be non-empty"); + } + // regex_rewrite + const auto* regex_rewrite = + envoy_config_route_v3_RouteAction_HashPolicy_Header_regex_rewrite( + header); if (regex_rewrite != nullptr) { - const envoy_type_matcher_v3_RegexMatcher* regex_matcher = + ValidationErrors::ScopedField field(errors, ".regex_rewrite.pattern"); + const auto* pattern = envoy_type_matcher_v3_RegexMatchAndSubstitute_pattern( regex_rewrite); - if (regex_matcher == nullptr) { - gpr_log( - GPR_DEBUG, - "RouteAction HashPolicy contains policy specifier Header with " - "RegexMatchAndSubstitution but RegexMatcher pattern is " - "missing"); + if (pattern == nullptr) { + errors->AddError("field not present"); + continue; + } + ValidationErrors::ScopedField field2(errors, ".regex"); + std::string regex = UpbStringToStdString( + envoy_type_matcher_v3_RegexMatcher_regex(pattern)); + if (regex.empty()) { + errors->AddError("field not present"); continue; } RE2::Options options; - policy.regex = std::make_unique( - UpbStringToStdString( - envoy_type_matcher_v3_RegexMatcher_regex(regex_matcher)), - options); - if (!policy.regex->ok()) { - gpr_log( - GPR_DEBUG, - "RouteAction HashPolicy contains policy specifier Header with " - "RegexMatchAndSubstitution but RegexMatcher pattern does not " - "compile"); + header_policy.regex = std::make_unique(regex, options); + if (!header_policy.regex->ok()) { + errors->AddError(absl::StrCat("errors compiling regex: ", + header_policy.regex->error())); continue; } - policy.regex_substitution = UpbStringToStdString( + header_policy.regex_substitution = UpbStringToStdString( envoy_type_matcher_v3_RegexMatchAndSubstitute_substitution( regex_rewrite)); } + policy.policy = std::move(header_policy); } else if ((filter_state = envoy_config_route_v3_RouteAction_HashPolicy_filter_state( hash_policy)) != nullptr) { + // filter_state std::string key = UpbStringToStdString( envoy_config_route_v3_RouteAction_HashPolicy_FilterState_key( filter_state)); - if (key == "io.grpc.channel_id") { - policy.type = XdsRouteConfigResource::Route::RouteAction::HashPolicy:: - Type::CHANNEL_ID; - } else { - gpr_log(GPR_DEBUG, - "RouteAction HashPolicy contains policy specifier " - "FilterState but " - "key is not io.grpc.channel_id."); - continue; - } + if (key != "io.grpc.channel_id") continue; + policy.policy = + XdsRouteConfigResource::Route::RouteAction::HashPolicy::ChannelId(); } else { - gpr_log(GPR_DEBUG, - "RouteAction HashPolicy contains unsupported policy specifier."); + // Unsupported hash policy type, ignore it. continue; } - route.hash_policies.emplace_back(std::move(policy)); + route_action.hash_policies.emplace_back(std::move(policy)); } // Get retry policy const envoy_config_route_v3_RetryPolicy* retry_policy = - envoy_config_route_v3_RouteAction_retry_policy(route_action); + envoy_config_route_v3_RouteAction_retry_policy(route_action_proto); if (retry_policy != nullptr) { - absl::optional retry; - absl::Status status = RetryPolicyParse(context, retry_policy, &retry); - if (!status.ok()) return status; - route.retry_policy = retry; + ValidationErrors::ScopedField field(errors, ".retry_policy"); + route_action.retry_policy = RetryPolicyParse(context, retry_policy, errors); + } + // Parse cluster specifier, which is one of several options. + if (envoy_config_route_v3_RouteAction_has_cluster(route_action_proto)) { + // Cluster name. + std::string cluster_name = UpbStringToStdString( + envoy_config_route_v3_RouteAction_cluster(route_action_proto)); + if (cluster_name.empty()) { + ValidationErrors::ScopedField field(errors, ".cluster"); + errors->AddError("must be non-empty"); + } + route_action.action = + XdsRouteConfigResource::Route::RouteAction::ClusterName{ + std::move(cluster_name)}; + } else if (envoy_config_route_v3_RouteAction_has_weighted_clusters( + route_action_proto)) { + // WeightedClusters. + ValidationErrors::ScopedField field(errors, ".weighted_clusters"); + const envoy_config_route_v3_WeightedCluster* weighted_clusters_proto = + envoy_config_route_v3_RouteAction_weighted_clusters(route_action_proto); + GPR_ASSERT(weighted_clusters_proto != nullptr); + std::vector + action_weighted_clusters; + size_t clusters_size; + const envoy_config_route_v3_WeightedCluster_ClusterWeight* const* clusters = + envoy_config_route_v3_WeightedCluster_clusters(weighted_clusters_proto, + &clusters_size); + for (size_t i = 0; i < clusters_size; ++i) { + ValidationErrors::ScopedField field(errors, + absl::StrCat(".clusters[", i, "]")); + const auto* cluster_proto = clusters[i]; + XdsRouteConfigResource::Route::RouteAction::ClusterWeight cluster; + // typed_per_filter_config + { + ValidationErrors::ScopedField field(errors, ".typed_per_filter_config"); + cluster.typed_per_filter_config = ParseTypedPerFilterConfig< + envoy_config_route_v3_WeightedCluster_ClusterWeight, + envoy_config_route_v3_WeightedCluster_ClusterWeight_TypedPerFilterConfigEntry>( + context, cluster_proto, + envoy_config_route_v3_WeightedCluster_ClusterWeight_typed_per_filter_config_next, + envoy_config_route_v3_WeightedCluster_ClusterWeight_TypedPerFilterConfigEntry_key, + envoy_config_route_v3_WeightedCluster_ClusterWeight_TypedPerFilterConfigEntry_value, + errors); + } + // name + cluster.name = UpbStringToStdString( + envoy_config_route_v3_WeightedCluster_ClusterWeight_name( + cluster_proto)); + if (cluster.name.empty()) { + ValidationErrors::ScopedField field(errors, ".name"); + errors->AddError("must be non-empty"); + } + // weight + const google_protobuf_UInt32Value* weight_proto = + envoy_config_route_v3_WeightedCluster_ClusterWeight_weight( + cluster_proto); + if (weight_proto == nullptr) { + ValidationErrors::ScopedField field(errors, ".weight"); + errors->AddError("field not present"); + } else { + cluster.weight = google_protobuf_UInt32Value_value(weight_proto); + if (cluster.weight == 0) continue; + } + // Add entry to WeightedClusters. + action_weighted_clusters.emplace_back(std::move(cluster)); + } + if (action_weighted_clusters.empty()) { + errors->AddError("no valid clusters specified"); + } + route_action.action = std::move(action_weighted_clusters); + } else if (XdsRlsEnabled() && + envoy_config_route_v3_RouteAction_has_cluster_specifier_plugin( + route_action_proto)) { + // ClusterSpecifierPlugin + ValidationErrors::ScopedField field(errors, ".cluster_specifier_plugin"); + std::string plugin_name = UpbStringToStdString( + envoy_config_route_v3_RouteAction_cluster_specifier_plugin( + route_action_proto)); + if (plugin_name.empty()) { + errors->AddError("must be non-empty"); + return absl::nullopt; + } + auto it = cluster_specifier_plugin_map.find(plugin_name); + if (it == cluster_specifier_plugin_map.end()) { + errors->AddError(absl::StrCat("unknown cluster specifier plugin name \"", + plugin_name, "\"")); + } else { + // If the cluster specifier config is empty, that means that the + // plugin was unsupported but optional. In that case, skip this route. + if (it->second.empty()) return absl::nullopt; + } + route_action.action = + XdsRouteConfigResource::Route::RouteAction::ClusterSpecifierPluginName{ + std::move(plugin_name)}; + } else { + // Not a supported cluster specifier, so ignore this route. + return absl::nullopt; + } + return route_action; +} + +absl::optional ParseRoute( + const XdsResourceType::DecodeContext& context, + const envoy_config_route_v3_Route* route_proto, + const absl::optional& + virtual_host_retry_policy, + const XdsRouteConfigResource::ClusterSpecifierPluginMap& + cluster_specifier_plugin_map, + std::set* cluster_specifier_plugins_not_seen, + ValidationErrors* errors) { + XdsRouteConfigResource::Route route; + // Parse route match. + { + ValidationErrors::ScopedField field(errors, ".match"); + const auto* match = envoy_config_route_v3_Route_match(route_proto); + if (match == nullptr) { + errors->AddError("field not present"); + return absl::nullopt; + } + // Skip routes with query_parameters set. + size_t query_parameters_size; + static_cast(envoy_config_route_v3_RouteMatch_query_parameters( + match, &query_parameters_size)); + if (query_parameters_size > 0) return absl::nullopt; + // Parse matchers. + auto path_matcher = RoutePathMatchParse(match, errors); + if (!path_matcher.has_value()) return absl::nullopt; + route.matchers.path_matcher = std::move(*path_matcher); + RouteHeaderMatchersParse(match, &route, errors); + RouteRuntimeFractionParse(match, &route, errors); + } + // Parse route action. + const envoy_config_route_v3_RouteAction* route_action_proto = + envoy_config_route_v3_Route_route(route_proto); + if (route_action_proto != nullptr) { + ValidationErrors::ScopedField field(errors, ".route"); + auto route_action = RouteActionParse(context, route_action_proto, + cluster_specifier_plugin_map, errors); + if (!route_action.has_value()) return absl::nullopt; + // If the route does not have a retry policy but the vhost does, + // use the vhost retry policy for this route. + if (!route_action->retry_policy.has_value()) { + route_action->retry_policy = virtual_host_retry_policy; + } + // Mark off plugins used in route action. + auto* cluster_specifier_action = absl::get_if< + XdsRouteConfigResource::Route::RouteAction::ClusterSpecifierPluginName>( + &route_action->action); + if (cluster_specifier_action != nullptr) { + cluster_specifier_plugins_not_seen->erase( + cluster_specifier_action->cluster_specifier_plugin_name); + } + route.action = std::move(*route_action); + } else if (envoy_config_route_v3_Route_has_non_forwarding_action( + route_proto)) { + route.action = XdsRouteConfigResource::Route::NonForwardingAction(); + } else { + // Leave route.action initialized to UnknownAction (its default). + } + // Parse typed_per_filter_config. + { + ValidationErrors::ScopedField field(errors, ".typed_per_filter_config"); + route.typed_per_filter_config = ParseTypedPerFilterConfig< + envoy_config_route_v3_Route, + envoy_config_route_v3_Route_TypedPerFilterConfigEntry>( + context, route_proto, + envoy_config_route_v3_Route_typed_per_filter_config_next, + envoy_config_route_v3_Route_TypedPerFilterConfigEntry_key, + envoy_config_route_v3_Route_TypedPerFilterConfigEntry_value, errors); } return route; } } // namespace -absl::StatusOr XdsRouteConfigResource::Parse( +XdsRouteConfigResource XdsRouteConfigResource::Parse( const XdsResourceType::DecodeContext& context, - const envoy_config_route_v3_RouteConfiguration* route_config) { + const envoy_config_route_v3_RouteConfiguration* route_config, + ValidationErrors* errors) { XdsRouteConfigResource rds_update; - // Get the cluster spcifier plugins + // Get the cluster spcifier plugin map. if (XdsRlsEnabled()) { - auto cluster_specifier_plugin_map = - ClusterSpecifierPluginParse(context, route_config); - if (!cluster_specifier_plugin_map.ok()) { - return cluster_specifier_plugin_map.status(); - } rds_update.cluster_specifier_plugin_map = - std::move(*cluster_specifier_plugin_map); + ClusterSpecifierPluginParse(context, route_config, errors); } - // Build a set of cluster_specifier_plugin configured to make sure each is - // actually referenced by a route action. - std::set cluster_specifier_plugins; + // Build a set of configured cluster_specifier_plugin names to make sure + // each is actually referenced by a route action. + std::set cluster_specifier_plugins_not_seen; for (auto& plugin : rds_update.cluster_specifier_plugin_map) { - cluster_specifier_plugins.emplace(plugin.first); + cluster_specifier_plugins_not_seen.emplace(plugin.first); } // Get the virtual hosts. size_t num_virtual_hosts; @@ -986,6 +1008,8 @@ absl::StatusOr XdsRouteConfigResource::Parse( envoy_config_route_v3_RouteConfiguration_virtual_hosts( route_config, &num_virtual_hosts); for (size_t i = 0; i < num_virtual_hosts; ++i) { + ValidationErrors::ScopedField field( + errors, absl::StrCat(".virtual_hosts[", i, "]")); rds_update.virtual_hosts.emplace_back(); XdsRouteConfigResource::VirtualHost& vhost = rds_update.virtual_hosts.back(); @@ -996,110 +1020,59 @@ absl::StatusOr XdsRouteConfigResource::Parse( for (size_t j = 0; j < domain_size; ++j) { std::string domain_pattern = UpbStringToStdString(domains[j]); if (!XdsRouting::IsValidDomainPattern(domain_pattern)) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid domain pattern \"", domain_pattern, "\".")); + ValidationErrors::ScopedField field(errors, + absl::StrCat(".domains[", j, "]")); + errors->AddError( + absl::StrCat("invalid domain pattern \"", domain_pattern, "\"")); } vhost.domains.emplace_back(std::move(domain_pattern)); } if (vhost.domains.empty()) { - return absl::InvalidArgumentError("VirtualHost has no domains"); + ValidationErrors::ScopedField field(errors, ".domains"); + errors->AddError("must be non-empty"); } // Parse typed_per_filter_config. - auto typed_per_filter_config = ParseTypedPerFilterConfig< - envoy_config_route_v3_VirtualHost, - envoy_config_route_v3_VirtualHost_TypedPerFilterConfigEntry>( - context, virtual_hosts[i], - envoy_config_route_v3_VirtualHost_typed_per_filter_config_next, - envoy_config_route_v3_VirtualHost_TypedPerFilterConfigEntry_key, - envoy_config_route_v3_VirtualHost_TypedPerFilterConfigEntry_value); - if (!typed_per_filter_config.ok()) { - return typed_per_filter_config.status(); + { + ValidationErrors::ScopedField field(errors, ".typed_per_filter_config"); + vhost.typed_per_filter_config = ParseTypedPerFilterConfig< + envoy_config_route_v3_VirtualHost, + envoy_config_route_v3_VirtualHost_TypedPerFilterConfigEntry>( + context, virtual_hosts[i], + envoy_config_route_v3_VirtualHost_typed_per_filter_config_next, + envoy_config_route_v3_VirtualHost_TypedPerFilterConfigEntry_key, + envoy_config_route_v3_VirtualHost_TypedPerFilterConfigEntry_value, + errors); } - vhost.typed_per_filter_config = std::move(*typed_per_filter_config); // Parse retry policy. absl::optional virtual_host_retry_policy; const envoy_config_route_v3_RetryPolicy* retry_policy = envoy_config_route_v3_VirtualHost_retry_policy(virtual_hosts[i]); if (retry_policy != nullptr) { - absl::Status status = - RetryPolicyParse(context, retry_policy, &virtual_host_retry_policy); - if (!status.ok()) return status; + ValidationErrors::ScopedField field(errors, ".retry_policy"); + virtual_host_retry_policy = + RetryPolicyParse(context, retry_policy, errors); } // Parse routes. + ValidationErrors::ScopedField field2(errors, ".routes"); + const size_t original_error_size = errors->size(); size_t num_routes; const envoy_config_route_v3_Route* const* routes = envoy_config_route_v3_VirtualHost_routes(virtual_hosts[i], &num_routes); - if (num_routes < 1) { - return absl::InvalidArgumentError("No route found in the virtual host."); - } - // Loop over the whole list of routes for (size_t j = 0; j < num_routes; ++j) { - const envoy_config_route_v3_RouteMatch* match = - envoy_config_route_v3_Route_match(routes[j]); - if (match == nullptr) { - return absl::InvalidArgumentError("Match can't be null."); - } - size_t query_parameters_size; - static_cast(envoy_config_route_v3_RouteMatch_query_parameters( - match, &query_parameters_size)); - if (query_parameters_size > 0) { - continue; - } - XdsRouteConfigResource::Route route; - bool ignore_route = false; - absl::Status status = RoutePathMatchParse(match, &route, &ignore_route); - if (!status.ok()) return status; - if (ignore_route) continue; - status = RouteHeaderMatchersParse(match, &route); - if (!status.ok()) return status; - status = RouteRuntimeFractionParse(match, &route); - if (!status.ok()) return status; - if (envoy_config_route_v3_Route_has_route(routes[j])) { - route.action.emplace(); - auto route_action = RouteActionParse( - context, routes[j], rds_update.cluster_specifier_plugin_map, - &ignore_route); - if (!route_action.ok()) return route_action.status(); - if (ignore_route) continue; - if (route_action->retry_policy == absl::nullopt && - retry_policy != nullptr) { - route_action->retry_policy = virtual_host_retry_policy; - } - // Mark off plugins used in route action. - auto* cluster_specifier_action = - absl::get_if(&route_action->action); - if (cluster_specifier_action != nullptr) { - cluster_specifier_plugins.erase( - cluster_specifier_action->cluster_specifier_plugin_name); - } - route.action = std::move(*route_action); - } else if (envoy_config_route_v3_Route_has_non_forwarding_action( - routes[j])) { - route.action - .emplace(); - } - auto typed_per_filter_config = ParseTypedPerFilterConfig< - envoy_config_route_v3_Route, - envoy_config_route_v3_Route_TypedPerFilterConfigEntry>( - context, routes[j], - envoy_config_route_v3_Route_typed_per_filter_config_next, - envoy_config_route_v3_Route_TypedPerFilterConfigEntry_key, - envoy_config_route_v3_Route_TypedPerFilterConfigEntry_value); - if (!typed_per_filter_config.ok()) { - return typed_per_filter_config.status(); - } - route.typed_per_filter_config = std::move(*typed_per_filter_config); - vhost.routes.emplace_back(std::move(route)); + ValidationErrors::ScopedField field(errors, absl::StrCat("[", j, "]")); + auto route = ParseRoute(context, routes[j], virtual_host_retry_policy, + rds_update.cluster_specifier_plugin_map, + &cluster_specifier_plugins_not_seen, errors); + if (route.has_value()) vhost.routes.emplace_back(std::move(*route)); } - if (vhost.routes.empty()) { - return absl::InvalidArgumentError("No valid routes specified."); + if (errors->size() == original_error_size && vhost.routes.empty()) { + errors->AddError("no valid routes in VirtualHost"); } } - // For plugins not used in route action, delete from the update to prevent - // further use. - for (auto& unused_plugin : cluster_specifier_plugins) { + // For cluster specifier plugins that were not used in any route action, + // delete them from the update, since they will never be used. + for (auto& unused_plugin : cluster_specifier_plugins_not_seen) { rds_update.cluster_specifier_plugin_map.erase(std::string(unused_plugin)); } return rds_update; @@ -1143,22 +1116,24 @@ XdsResourceType::DecodeResult XdsRouteConfigResourceType::Decode( // Validate resource. result.name = UpbStringToStdString( envoy_config_route_v3_RouteConfiguration_name(resource)); - auto rds_update = XdsRouteConfigResource::Parse(context, resource); - if (!rds_update.ok()) { + ValidationErrors errors; + auto rds_update = XdsRouteConfigResource::Parse(context, resource, &errors); + if (!errors.ok()) { + absl::Status status = + errors.status("errors validating RouteConfiguration resource"); if (GRPC_TRACE_FLAG_ENABLED(*context.tracer)) { gpr_log(GPR_ERROR, "[xds_client %p] invalid RouteConfiguration %s: %s", - context.client, result.name->c_str(), - rds_update.status().ToString().c_str()); + context.client, result.name->c_str(), status.ToString().c_str()); } - result.resource = rds_update.status(); + result.resource = std::move(status); } else { if (GRPC_TRACE_FLAG_ENABLED(*context.tracer)) { gpr_log(GPR_INFO, "[xds_client %p] parsed RouteConfiguration %s: %s", context.client, result.name->c_str(), - rds_update->ToString().c_str()); + rds_update.ToString().c_str()); } result.resource = - std::make_unique(std::move(*rds_update)); + std::make_unique(std::move(rds_update)); } return result; } diff --git a/src/core/ext/xds/xds_route_config.h b/src/core/ext/xds/xds_route_config.h index 738f65786d5..417b52a1b8d 100644 --- a/src/core/ext/xds/xds_route_config.h +++ b/src/core/ext/xds/xds_route_config.h @@ -27,7 +27,6 @@ #include #include -#include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" #include "absl/types/variant.h" @@ -44,6 +43,7 @@ #include "src/core/ext/xds/xds_resource_type_impl.h" #include "src/core/lib/channel/status_util.h" #include "src/core/lib/gprpp/time.h" +#include "src/core/lib/gprpp/validation_errors.h" #include "src/core/lib/matchers/matchers.h" namespace grpc_core { @@ -102,25 +102,35 @@ struct XdsRouteConfigResource : public XdsResourceType::ResourceData { struct RouteAction { struct HashPolicy { - enum Type { HEADER, CHANNEL_ID }; - Type type; - bool terminal = false; - // Fields used for type HEADER. - std::string header_name; - std::unique_ptr regex = nullptr; - std::string regex_substitution; + struct Header { + std::string header_name; + std::unique_ptr regex; + std::string regex_substitution; + + Header() = default; + + // Copyable. + Header(const Header& other); + Header& operator=(const Header& other); - HashPolicy() {} + // Movable. + Header(Header&& other) noexcept; + Header& operator=(Header&& other) noexcept; - // Copyable. - HashPolicy(const HashPolicy& other); - HashPolicy& operator=(const HashPolicy& other); + bool operator==(const Header& other) const; + std::string ToString() const; + }; - // Moveable. - HashPolicy(HashPolicy&& other) noexcept; - HashPolicy& operator=(HashPolicy&& other) noexcept; + struct ChannelId { + bool operator==(const ChannelId&) const { return true; } + }; - bool operator==(const HashPolicy& other) const; + absl::variant policy; + bool terminal = false; + + bool operator==(const HashPolicy& other) const { + return policy == other.policy && terminal == other.terminal; + } std::string ToString() const; }; @@ -210,9 +220,10 @@ struct XdsRouteConfigResource : public XdsResourceType::ResourceData { } std::string ToString() const; - static absl::StatusOr Parse( + static XdsRouteConfigResource Parse( const XdsResourceType::DecodeContext& context, - const envoy_config_route_v3_RouteConfiguration* route_config); + const envoy_config_route_v3_RouteConfiguration* route_config, + ValidationErrors* errors); }; class XdsRouteConfigResourceType diff --git a/src/core/lib/channel/status_util.cc b/src/core/lib/channel/status_util.cc index b943ab797ac..9af7c231812 100644 --- a/src/core/lib/channel/status_util.cc +++ b/src/core/lib/channel/status_util.cc @@ -22,7 +22,11 @@ #include +#include +#include + #include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" #include "src/core/lib/gpr/useful.h" @@ -114,6 +118,20 @@ bool grpc_status_code_from_int(int status_int, grpc_status_code* status) { namespace grpc_core { +namespace internal { + +std::string StatusCodeSet::ToString() const { + std::vector codes; + for (size_t i = 0; i < GPR_ARRAY_SIZE(g_status_string_entries); ++i) { + if (Contains(g_status_string_entries[i].status)) { + codes.emplace_back(g_status_string_entries[i].str); + } + } + return absl::StrCat("{", absl::StrJoin(codes, ","), "}"); +} + +} // namespace internal + absl::Status MaybeRewriteIllegalStatusCode(absl::Status status, absl::string_view source) { switch (status.code()) { diff --git a/src/core/lib/channel/status_util.h b/src/core/lib/channel/status_util.h index df0fc53f8ad..b43f2aa5baa 100644 --- a/src/core/lib/channel/status_util.h +++ b/src/core/lib/channel/status_util.h @@ -21,6 +21,8 @@ #include +#include + #include "absl/status/status.h" #include "absl/strings/string_view.h" @@ -47,7 +49,10 @@ class StatusCodeSet { public: bool Empty() const { return status_code_mask_ == 0; } - void Add(grpc_status_code status) { status_code_mask_ |= (1 << status); } + StatusCodeSet& Add(grpc_status_code status) { + status_code_mask_ |= (1 << status); + return *this; + } bool Contains(grpc_status_code status) const { return status_code_mask_ & (1 << status); @@ -57,6 +62,8 @@ class StatusCodeSet { return status_code_mask_ == other.status_code_mask_; } + std::string ToString() const; + private: int status_code_mask_ = 0; // A bitfield of status codes in the set. }; diff --git a/src/core/lib/matchers/matchers.cc b/src/core/lib/matchers/matchers.cc index fa00b49f2eb..a32ceeed3dc 100644 --- a/src/core/lib/matchers/matchers.cc +++ b/src/core/lib/matchers/matchers.cc @@ -22,6 +22,7 @@ #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" namespace grpc_core { @@ -37,7 +38,8 @@ absl::StatusOr StringMatcher::Create(Type type, auto regex_matcher = std::make_unique(std::string(matcher)); if (!regex_matcher->ok()) { return absl::InvalidArgumentError( - "Invalid regex string specified in matcher."); + absl::StrCat("Invalid regex string specified in matcher: ", + regex_matcher->error())); } return StringMatcher(std::move(regex_matcher)); } else { diff --git a/src/proto/grpc/testing/xds/v3/route.proto b/src/proto/grpc/testing/xds/v3/route.proto index 8a8a2cec681..6c064b4edc4 100644 --- a/src/proto/grpc/testing/xds/v3/route.proto +++ b/src/proto/grpc/testing/xds/v3/route.proto @@ -188,6 +188,8 @@ message RouteMatch { // on :path, etc. The issue with that is it is unclear how to generically deal with query string // stripping. This needs more thought.] type.matcher.v3.RegexMatcher safe_regex = 10; + + string path_separated_prefix = 14; } // Indicates that prefix/path matching should be case insensitive. The default diff --git a/test/core/security/matchers_test.cc b/test/core/security/matchers_test.cc index d088b572103..c53aced1e36 100644 --- a/test/core/security/matchers_test.cc +++ b/test/core/security/matchers_test.cc @@ -86,7 +86,9 @@ TEST(StringMatcherTest, InvalidRegex) { EXPECT_FALSE(string_matcher.ok()); EXPECT_EQ(string_matcher.status().code(), absl::StatusCode::kInvalidArgument); EXPECT_EQ(string_matcher.status().message(), - "Invalid regex string specified in matcher."); + "Invalid regex string specified in matcher: " + "invalid character class range: b-a") + << string_matcher.status(); } TEST(StringMatcherTest, SafeRegexMatchCaseSensitive) { @@ -161,7 +163,9 @@ TEST(HeaderMatcherTest, InvalidRegex) { EXPECT_FALSE(header_matcher.ok()); EXPECT_EQ(header_matcher.status().code(), absl::StatusCode::kInvalidArgument); EXPECT_EQ(header_matcher.status().message(), - "Invalid regex string specified in matcher."); + "Invalid regex string specified in matcher: " + "invalid character class range: b-a") + << header_matcher.status(); } TEST(HeaderMatcherTest, RangeMatcherValidRange) { diff --git a/test/core/xds/BUILD b/test/core/xds/BUILD index fc84d8644fe..63f9a1fed10 100644 --- a/test/core/xds/BUILD +++ b/test/core/xds/BUILD @@ -245,6 +245,28 @@ grpc_cc_test( ], ) +grpc_cc_test( + name = "xds_route_config_resource_type_test", + srcs = ["xds_route_config_resource_type_test.cc"], + external_deps = ["gtest"], + language = "C++", + uses_event_engine = False, + uses_polling = False, + deps = [ + "//:gpr", + "//:grpc", + "//src/core:grpc_xds_client", + "//src/proto/grpc/lookup/v1:rls_config_proto", + "//src/proto/grpc/testing/xds/v3:fault_proto", + "//src/proto/grpc/testing/xds/v3:http_filter_rbac_proto", + "//src/proto/grpc/testing/xds/v3:route_proto", + "//src/proto/grpc/testing/xds/v3:typed_struct_proto", + "//test/core/util:grpc_test_util", + "//test/core/util:scoped_env_var", + "//test/cpp/util:grpc_cli_utils", + ], +) + grpc_cc_test( name = "xds_cluster_resource_type_test", srcs = ["xds_cluster_resource_type_test.cc"], diff --git a/test/core/xds/xds_listener_resource_type_test.cc b/test/core/xds/xds_listener_resource_type_test.cc index 94aabec6abd..85f18644d35 100644 --- a/test/core/xds/xds_listener_resource_type_test.cc +++ b/test/core/xds/xds_listener_resource_type_test.cc @@ -578,37 +578,6 @@ TEST_P(HttpConnectionManagerTest, HttpFilterMissingConfig) { << decode_result.resource.status(); } -TEST_P(HttpConnectionManagerTest, HttpFilterMissingConfigButOptional) { - HttpConnectionManager hcm; - auto* filter = hcm.add_http_filters(); - filter->set_name("foo"); - filter->set_is_optional(true); - filter = hcm.add_http_filters(); - filter->set_name("router"); - filter->mutable_typed_config()->PackFrom(Router()); - auto* rds = hcm.mutable_rds(); - rds->set_route_config_name("rds_name"); - rds->mutable_config_source()->mutable_self(); - Listener listener = MakeListener(hcm); - std::string serialized_resource; - ASSERT_TRUE(listener.SerializeToString(&serialized_resource)); - auto* resource_type = XdsListenerResourceType::Get(); - auto decode_result = - resource_type->Decode(decode_context_, serialized_resource); - ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); - ASSERT_TRUE(decode_result.name.has_value()); - EXPECT_EQ(*decode_result.name, "foo"); - auto& resource = static_cast(**decode_result.resource); - auto http_connection_manager = GetHCMConfig(resource); - ASSERT_TRUE(http_connection_manager.has_value()); - ASSERT_EQ(http_connection_manager->http_filters.size(), 1UL); - auto& router = http_connection_manager->http_filters[0]; - EXPECT_EQ(router.name, "router"); - EXPECT_EQ(router.config.config_proto_type_name, - "envoy.extensions.filters.http.router.v3.Router"); - EXPECT_EQ(router.config.config, Json()) << router.config.config.Dump(); -} - TEST_P(HttpConnectionManagerTest, HttpFilterTypeNotSupported) { HttpConnectionManager hcm; auto* filter = hcm.add_http_filters(); diff --git a/test/core/xds/xds_route_config_resource_type_test.cc b/test/core/xds/xds_route_config_resource_type_test.cc new file mode 100644 index 00000000000..07f74f0d0e8 --- /dev/null +++ b/test/core/xds/xds_route_config_resource_type_test.cc @@ -0,0 +1,2167 @@ +// +// Copyright 2022 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "absl/types/variant.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "re2/re2.h" +#include "upb/def.hpp" +#include "upb/upb.hpp" + +#include +#include +#include + +#include "src/core/ext/xds/xds_bootstrap.h" +#include "src/core/ext/xds/xds_bootstrap_grpc.h" +#include "src/core/ext/xds/xds_client.h" +#include "src/core/ext/xds/xds_http_filters.h" +#include "src/core/ext/xds/xds_resource_type.h" +#include "src/core/ext/xds/xds_route_config.h" +#include "src/core/lib/channel/status_util.h" +#include "src/core/lib/debug/trace.h" +#include "src/core/lib/gprpp/ref_counted_ptr.h" +#include "src/core/lib/gprpp/time.h" +#include "src/core/lib/iomgr/error.h" +#include "src/core/lib/json/json.h" +#include "src/core/lib/matchers/matchers.h" +#include "src/proto/grpc/lookup/v1/rls_config.pb.h" +#include "src/proto/grpc/testing/xds/v3/base.pb.h" +#include "src/proto/grpc/testing/xds/v3/extension.pb.h" +#include "src/proto/grpc/testing/xds/v3/fault.pb.h" +#include "src/proto/grpc/testing/xds/v3/percent.pb.h" +#include "src/proto/grpc/testing/xds/v3/range.pb.h" +#include "src/proto/grpc/testing/xds/v3/regex.pb.h" +#include "src/proto/grpc/testing/xds/v3/route.pb.h" +#include "src/proto/grpc/testing/xds/v3/typed_struct.pb.h" +#include "test/core/util/scoped_env_var.h" +#include "test/core/util/test_config.h" + +using envoy::config::route::v3::RouteConfiguration; +using grpc::lookup::v1::RouteLookupClusterSpecifier; + +namespace grpc_core { +namespace testing { +namespace { + +TraceFlag xds_route_config_resource_type_test_trace( + true, "xds_route_config_resource_type_test"); + +class XdsRouteConfigTest : public ::testing::Test { + protected: + XdsRouteConfigTest() + : xds_client_(MakeXdsClient()), + decode_context_{xds_client_.get(), xds_client_->bootstrap().server(), + &xds_route_config_resource_type_test_trace, + upb_def_pool_.ptr(), upb_arena_.ptr()} {} + + static RefCountedPtr MakeXdsClient() { + grpc_error_handle error; + auto bootstrap = GrpcXdsBootstrap::Create( + "{\n" + " \"xds_servers\": [\n" + " {\n" + " \"server_uri\": \"xds.example.com\",\n" + " \"channel_creds\": [\n" + " {\"type\": \"google_default\"}\n" + " ]\n" + " }\n" + " ]\n" + "}"); + if (!bootstrap.ok()) { + gpr_log(GPR_ERROR, "Error parsing bootstrap: %s", + bootstrap.status().ToString().c_str()); + GPR_ASSERT(false); + } + return MakeRefCounted(std::move(*bootstrap), + /*transport_factory=*/nullptr); + } + + RefCountedPtr xds_client_; + upb::DefPool upb_def_pool_; + upb::Arena upb_arena_; + XdsResourceType::DecodeContext decode_context_; +}; + +TEST_F(XdsRouteConfigTest, Definition) { + auto* resource_type = XdsRouteConfigResourceType::Get(); + ASSERT_NE(resource_type, nullptr); + EXPECT_EQ(resource_type->type_url(), + "envoy.config.route.v3.RouteConfiguration"); + EXPECT_FALSE(resource_type->AllResourcesRequiredInSotW()); +} + +TEST_F(XdsRouteConfigTest, UnparseableProto) { + std::string serialized_resource("\0", 1); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "Can't parse RouteConfiguration resource.") + << decode_result.resource.status(); +} + +TEST_F(XdsRouteConfigTest, MinimumValidConfig) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT(resource.cluster_specifier_plugin_map, ::testing::ElementsAre()); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + EXPECT_THAT(resource.virtual_hosts[0].domains, ::testing::ElementsAre("*")); + EXPECT_THAT(resource.virtual_hosts[0].typed_per_filter_config, + ::testing::ElementsAre()); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto& matchers = route.matchers; + EXPECT_EQ(matchers.path_matcher.ToString(), "StringMatcher{prefix=}"); + EXPECT_THAT(matchers.header_matchers, ::testing::ElementsAre()); + EXPECT_FALSE(matchers.fraction_per_million.has_value()); + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto* cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster1"); + EXPECT_THAT(action->hash_policies, ::testing::ElementsAre()); + EXPECT_FALSE(action->retry_policy.has_value()); + EXPECT_FALSE(action->max_stream_duration.has_value()); + EXPECT_THAT(route.typed_per_filter_config, ::testing::ElementsAre()); +} + +// +// virtual host tests +// + +using VirtualHostTest = XdsRouteConfigTest; + +TEST_F(VirtualHostTest, MultipleVirtualHosts) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + vhost = route_config.add_virtual_hosts(); + vhost->add_domains("foo.example.com"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster2"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT(resource.cluster_specifier_plugin_map, ::testing::ElementsAre()); + ASSERT_EQ(resource.virtual_hosts.size(), 2UL); + EXPECT_THAT(resource.virtual_hosts[0].domains, ::testing::ElementsAre("*")); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto* route = &resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route->action); + ASSERT_NE(action, nullptr); + auto* cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster1"); + EXPECT_THAT(resource.virtual_hosts[1].domains, + ::testing::ElementsAre("foo.example.com")); + ASSERT_EQ(resource.virtual_hosts[1].routes.size(), 1UL); + route = &resource.virtual_hosts[1].routes[0]; + action = + absl::get_if(&route->action); + ASSERT_NE(action, nullptr); + cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster2"); +} + +TEST_F(VirtualHostTest, BadDomainPattern) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("foo*bar"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].domains[0] " + "error:invalid domain pattern \"foo*bar\"]") + << decode_result.resource.status(); +} + +TEST_F(VirtualHostTest, NoDomainsSpecified) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].domains error:must be non-empty]") + << decode_result.resource.status(); +} + +TEST_F(VirtualHostTest, NoRoutesInVirtualHost) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes " + "error:no valid routes in VirtualHost]") + << decode_result.resource.status(); +} + +// +// typed_per_filter_config tests +// + +// These tests cover common handling of typed_per_filter_config at all +// three layers (virtual host, route, and weighted cluster), so we run +// them in all three contexts. +class TypedPerFilterConfigScope { + public: + enum Scope { kVirtualHost, kRoute, kWeightedCluster }; + + explicit TypedPerFilterConfigScope(Scope scope) : scope_(scope) {} + + Scope scope() const { return scope_; } + + // For use as the final parameter in INSTANTIATE_TEST_SUITE_P(). + static std::string Name( + const ::testing::TestParamInfo& info) { + switch (info.param.scope_) { + case kVirtualHost: + return "VirtualHost"; + case kRoute: + return "Route"; + case kWeightedCluster: + return "WeightedCluster"; + default: + break; + } + GPR_UNREACHABLE_CODE(return "UNKNOWN"); + } + + private: + Scope scope_; +}; + +class TypedPerFilterConfigTest + : public XdsRouteConfigTest, + public ::testing::WithParamInterface { + protected: + TypedPerFilterConfigTest() { + route_config_.set_name("foo"); + auto* vhost = route_config_.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + } + + static google::protobuf::Map* + GetTypedPerFilterConfigProto(RouteConfiguration* route_config) { + switch (GetParam().scope()) { + case TypedPerFilterConfigScope::kVirtualHost: + return route_config->mutable_virtual_hosts(0) + ->mutable_typed_per_filter_config(); + case TypedPerFilterConfigScope::kRoute: + return route_config->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_typed_per_filter_config(); + case TypedPerFilterConfigScope::kWeightedCluster: { + auto* cluster = route_config->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->mutable_weighted_clusters() + ->add_clusters(); + cluster->set_name("cluster1"); + cluster->mutable_weight()->set_value(1); + return cluster->mutable_typed_per_filter_config(); + } + default: + break; + } + GPR_UNREACHABLE_CODE(return nullptr); + } + + static const XdsRouteConfigResource::TypedPerFilterConfig& + GetTypedPerFilterConfig(const XdsRouteConfigResource& resource) { + switch (GetParam().scope()) { + case TypedPerFilterConfigScope::kVirtualHost: + return resource.virtual_hosts[0].typed_per_filter_config; + case TypedPerFilterConfigScope::kRoute: + return resource.virtual_hosts[0].routes[0].typed_per_filter_config; + case TypedPerFilterConfigScope::kWeightedCluster: { + auto& action = absl::get( + resource.virtual_hosts[0].routes[0].action); + auto& weighted_clusters = absl::get>( + action.action); + return weighted_clusters[0].typed_per_filter_config; + } + default: + break; + } + GPR_ASSERT(false); + } + + static absl::string_view FieldName() { + switch (GetParam().scope()) { + case TypedPerFilterConfigScope::kVirtualHost: + return "virtual_hosts[0].typed_per_filter_config"; + case TypedPerFilterConfigScope::kRoute: + return "virtual_hosts[0].routes[0].typed_per_filter_config"; + case TypedPerFilterConfigScope::kWeightedCluster: + return "virtual_hosts[0].routes[0].route.weighted_clusters" + ".clusters[0].typed_per_filter_config"; + default: + break; + } + GPR_ASSERT(false); + } + + RouteConfiguration route_config_; +}; + +INSTANTIATE_TEST_SUITE_P( + XdsRouteConfig, TypedPerFilterConfigTest, + ::testing::Values( + TypedPerFilterConfigScope(TypedPerFilterConfigScope::kVirtualHost), + TypedPerFilterConfigScope(TypedPerFilterConfigScope::kRoute), + TypedPerFilterConfigScope(TypedPerFilterConfigScope::kWeightedCluster)), + &TypedPerFilterConfigScope::Name); + +TEST_P(TypedPerFilterConfigTest, Basic) { + envoy::extensions::filters::http::fault::v3::HTTPFault fault_config; + fault_config.mutable_abort()->set_grpc_status(GRPC_STATUS_PERMISSION_DENIED); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(fault_config); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + auto& typed_per_filter_config = GetTypedPerFilterConfig(resource); + ASSERT_EQ(typed_per_filter_config.size(), 1UL); + auto it = typed_per_filter_config.begin(); + ASSERT_NE(it, typed_per_filter_config.end()); + EXPECT_EQ("fault", it->first); + const auto& filter_config = it->second; + EXPECT_EQ(filter_config.config_proto_type_name, + "envoy.extensions.filters.http.fault.v3.HTTPFault"); + EXPECT_EQ(filter_config.config.Dump(), + "{\"abortCode\":\"PERMISSION_DENIED\"}"); +} + +TEST_P(TypedPerFilterConfigTest, EmptyName) { + envoy::extensions::filters::http::fault::v3::HTTPFault fault_config; + fault_config.mutable_abort()->set_grpc_status(GRPC_STATUS_PERMISSION_DENIED); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)[""].PackFrom(fault_config); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat("errors validating RouteConfiguration resource: [field:", + FieldName(), "[] error:filter name must be non-empty]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, EmptyConfig) { + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"]; + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat("errors validating RouteConfiguration resource: [field:", + FieldName(), "[fault].type_url error:field not present]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, UnsupportedFilterType) { + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(RouteConfiguration()); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + "[fault].value[envoy.config.route.v3.RouteConfiguration] " + "error:unsupported filter type]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, FilterConfigInvalid) { + envoy::extensions::filters::http::fault::v3::HTTPFault fault_config; + fault_config.mutable_abort()->set_grpc_status(123); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(fault_config); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + "[fault].value[envoy.extensions.filters.http.fault.v3.HTTPFault]" + ".abort.grpc_status " + "error:invalid gRPC status code: 123]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, FilterConfigWrapper) { + envoy::extensions::filters::http::fault::v3::HTTPFault fault_config; + fault_config.mutable_abort()->set_grpc_status(GRPC_STATUS_PERMISSION_DENIED); + envoy::config::route::v3::FilterConfig filter_config_wrapper; + filter_config_wrapper.mutable_config()->PackFrom(fault_config); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(filter_config_wrapper); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + auto& typed_per_filter_config = GetTypedPerFilterConfig(resource); + ASSERT_EQ(typed_per_filter_config.size(), 1UL); + auto it = typed_per_filter_config.begin(); + ASSERT_NE(it, typed_per_filter_config.end()); + EXPECT_EQ("fault", it->first); + const auto& filter_config = it->second; + EXPECT_EQ(filter_config.config_proto_type_name, + "envoy.extensions.filters.http.fault.v3.HTTPFault"); + EXPECT_EQ(filter_config.config.Dump(), + "{\"abortCode\":\"PERMISSION_DENIED\"}"); +} + +TEST_P(TypedPerFilterConfigTest, FilterConfigWrapperInTypedStruct) { + xds::type::v3::TypedStruct typed_struct; + typed_struct.set_type_url( + "types.googleapis.com/envoy.config.route.v3.FilterConfig"); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(typed_struct); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + "[fault].value[xds.type.v3.TypedStruct].value[" + "envoy.config.route.v3.FilterConfig] " + "error:could not parse FilterConfig]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, FilterConfigWrapperUnparseable) { + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + auto& any = (*typed_per_filter_config_proto)["fault"]; + any.set_type_url("types.googleapis.com/envoy.config.route.v3.FilterConfig"); + any.set_value(std::string("\0", 1)); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + "[fault].value[envoy.config.route.v3.FilterConfig] " + "error:could not parse FilterConfig]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, FilterConfigWrapperEmptyConfig) { + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom( + envoy::config::route::v3::FilterConfig()); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + "[fault].value[envoy.config.route.v3.FilterConfig].config " + "error:field not present]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, FilterConfigWrapperUnsupportedFilterType) { + envoy::config::route::v3::FilterConfig filter_config_wrapper; + filter_config_wrapper.mutable_config()->PackFrom(RouteConfiguration()); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(filter_config_wrapper); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + "[fault].value[envoy.config.route.v3.FilterConfig].config.value[" + "envoy.config.route.v3.RouteConfiguration] " + "error:unsupported filter type]")) + << decode_result.resource.status(); +} + +TEST_P(TypedPerFilterConfigTest, + FilterConfigWrapperUnsupportedOptionalFilterType) { + envoy::config::route::v3::FilterConfig filter_config_wrapper; + filter_config_wrapper.mutable_config()->PackFrom(RouteConfiguration()); + filter_config_wrapper.set_is_optional(true); + auto* typed_per_filter_config_proto = + GetTypedPerFilterConfigProto(&route_config_); + (*typed_per_filter_config_proto)["fault"].PackFrom(filter_config_wrapper); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + auto& typed_per_filter_config = GetTypedPerFilterConfig(resource); + EXPECT_THAT(typed_per_filter_config, ::testing::ElementsAre()); +} + +// +// retry policy tests +// + +// These tests cover common handling of retry policy at both the virtual +// host and route layer, so we run them in both contexts. +class RetryPolicyScope { + public: + enum Scope { kVirtualHost, kRoute }; + + explicit RetryPolicyScope(Scope scope) : scope_(scope) {} + + Scope scope() const { return scope_; } + + // For use as the final parameter in INSTANTIATE_TEST_SUITE_P(). + static std::string Name( + const ::testing::TestParamInfo& info) { + switch (info.param.scope_) { + case kVirtualHost: + return "VirtualHost"; + case kRoute: + return "Route"; + default: + break; + } + GPR_UNREACHABLE_CODE(return "UNKNOWN"); + } + + private: + Scope scope_; +}; + +class RetryPolicyTest : public XdsRouteConfigTest, + public ::testing::WithParamInterface { + protected: + RetryPolicyTest() { + route_config_.set_name("foo"); + auto* vhost = route_config_.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + } + + static envoy::config::route::v3::RetryPolicy* GetRetryPolicyProto( + RouteConfiguration* route_config) { + switch (GetParam().scope()) { + case RetryPolicyScope::kVirtualHost: + return route_config->mutable_virtual_hosts(0)->mutable_retry_policy(); + case RetryPolicyScope::kRoute: + return route_config->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->mutable_retry_policy(); + default: + break; + } + GPR_UNREACHABLE_CODE(return nullptr); + } + + static absl::string_view FieldName() { + switch (GetParam().scope()) { + case RetryPolicyScope::kVirtualHost: + return "virtual_hosts[0].retry_policy"; + case RetryPolicyScope::kRoute: + return "virtual_hosts[0].routes[0].route.retry_policy"; + default: + break; + } + GPR_ASSERT(false); + } + + RouteConfiguration route_config_; +}; + +INSTANTIATE_TEST_SUITE_P( + XdsRouteConfig, RetryPolicyTest, + ::testing::Values(RetryPolicyScope(RetryPolicyScope::kVirtualHost), + RetryPolicyScope(RetryPolicyScope::kRoute)), + &RetryPolicyScope::Name); + +TEST_P(RetryPolicyTest, Empty) { + GetRetryPolicyProto(&route_config_); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + ASSERT_TRUE(action->retry_policy.has_value()); + const auto& retry_policy = *action->retry_policy; + // Defaults. + auto expected_codes = internal::StatusCodeSet(); + EXPECT_EQ(retry_policy.retry_on, expected_codes) + << "Actual: " << retry_policy.retry_on.ToString() + << "\nExpected: " << expected_codes.ToString(); + EXPECT_EQ(retry_policy.num_retries, 1); + EXPECT_EQ(retry_policy.retry_back_off.base_interval, + Duration::Milliseconds(25)); + EXPECT_EQ(retry_policy.retry_back_off.max_interval, + Duration::Milliseconds(250)); +} + +TEST_P(RetryPolicyTest, AllFields) { + auto* retry_policy_proto = GetRetryPolicyProto(&route_config_); + retry_policy_proto->set_retry_on( + "cancelled,deadline-exceeded,internal,some-unsupported-policy," + "resource-exhausted,unavailable"); + retry_policy_proto->mutable_num_retries()->set_value(3); + auto* backoff = retry_policy_proto->mutable_retry_back_off(); + backoff->mutable_base_interval()->set_seconds(1); + backoff->mutable_max_interval()->set_seconds(3); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + ASSERT_TRUE(action->retry_policy.has_value()); + const auto& retry_policy = *action->retry_policy; + auto expected_codes = internal::StatusCodeSet() + .Add(GRPC_STATUS_CANCELLED) + .Add(GRPC_STATUS_DEADLINE_EXCEEDED) + .Add(GRPC_STATUS_INTERNAL) + .Add(GRPC_STATUS_RESOURCE_EXHAUSTED) + .Add(GRPC_STATUS_UNAVAILABLE); + EXPECT_EQ(retry_policy.retry_on, expected_codes) + << "Actual: " << retry_policy.retry_on.ToString() + << "\nExpected: " << expected_codes.ToString(); + EXPECT_EQ(retry_policy.num_retries, 3); + EXPECT_EQ(retry_policy.retry_back_off.base_interval, Duration::Seconds(1)); + EXPECT_EQ(retry_policy.retry_back_off.max_interval, Duration::Seconds(3)); +} + +TEST_P(RetryPolicyTest, MaxIntervalDefaultsTo10xBaseInterval) { + auto* retry_policy_proto = GetRetryPolicyProto(&route_config_); + retry_policy_proto->mutable_retry_back_off() + ->mutable_base_interval() + ->set_seconds(3); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + ASSERT_TRUE(action->retry_policy.has_value()); + const auto& retry_policy = *action->retry_policy; + EXPECT_EQ(retry_policy.retry_back_off.base_interval, Duration::Seconds(3)); + EXPECT_EQ(retry_policy.retry_back_off.max_interval, Duration::Seconds(30)); +} + +TEST_P(RetryPolicyTest, InvalidValues) { + auto* retry_policy_proto = GetRetryPolicyProto(&route_config_); + retry_policy_proto->set_retry_on("unavailable"); + retry_policy_proto->mutable_num_retries()->set_value(0); + auto* backoff = retry_policy_proto->mutable_retry_back_off(); + backoff->mutable_base_interval()->set_seconds(315576000001); + backoff->mutable_max_interval()->set_seconds(315576000001); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + ".num_retries error:must be greater than 0; field:", FieldName(), + ".retry_back_off.base_interval.seconds " + "error:value must be in the range [0, 315576000000]; field:", + FieldName(), + ".retry_back_off.max_interval.seconds " + "error:value must be in the range [0, 315576000000]]")) + << decode_result.resource.status(); +} + +TEST_P(RetryPolicyTest, MissingBaseInterval) { + auto* retry_policy_proto = GetRetryPolicyProto(&route_config_); + retry_policy_proto->mutable_retry_back_off(); + std::string serialized_resource; + ASSERT_TRUE(route_config_.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + decode_result.resource.status().message(), + absl::StrCat( + "errors validating RouteConfiguration resource: [field:", FieldName(), + ".retry_back_off.base_interval error:field not present]")) + << decode_result.resource.status(); +} + +using RetryPolicyOverrideTest = XdsRouteConfigTest; + +TEST_F(RetryPolicyOverrideTest, RoutePolicyOverridesVhostPolicy) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + vhost->mutable_retry_policy()->set_retry_on("unavailable"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_route()->mutable_retry_policy()->set_retry_on( + "cancelled"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + ASSERT_TRUE(action->retry_policy.has_value()); + auto expected_codes = internal::StatusCodeSet().Add(GRPC_STATUS_CANCELLED); + EXPECT_EQ(action->retry_policy->retry_on, expected_codes) + << "Actual: " << action->retry_policy->retry_on.ToString() + << "\nExpected: " << expected_codes.ToString(); +} + +// +// route match tests +// + +using RouteMatchTest = XdsRouteConfigTest; + +TEST_F(RouteMatchTest, RouteMatchNotPresent) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].match " + "error:field not present]") + << decode_result.resource.status(); +} + +TEST_F(RouteMatchTest, PathMatchers) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path("/service/method"); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster2"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->mutable_safe_regex()->set_regex("/.*"); + route_proto->mutable_route()->set_cluster("cluster3"); + // The remaining routes will be ignored, since they cannot possibly + // match a gRPC path. + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix("does_not_start_with_slash"); + route_proto->mutable_route()->set_cluster("cluster4"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix("/three/slashes/"); + route_proto->mutable_route()->set_cluster("cluster5"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix("//two_leading_slashes"); + route_proto->mutable_route()->set_cluster("cluster6"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path(""); + route_proto->mutable_route()->set_cluster("cluster7"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path("does_not_start_with_slash"); + route_proto->mutable_route()->set_cluster("cluster8"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path("/three/slashes/"); + route_proto->mutable_route()->set_cluster("cluster9"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path("/one_slash"); + route_proto->mutable_route()->set_cluster("cluster10"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path("//service_empty"); + route_proto->mutable_route()->set_cluster("cluster11"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path("/method_empty/"); + route_proto->mutable_route()->set_cluster("cluster12"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + auto& virtual_host = resource.virtual_hosts.front(); + ASSERT_EQ(virtual_host.routes.size(), 3UL); + // route 0 + EXPECT_EQ(virtual_host.routes[0].matchers.path_matcher.ToString(), + "StringMatcher{exact=/service/method}"); + auto* action = absl::get_if( + &virtual_host.routes[0].action); + ASSERT_NE(action, nullptr); + auto* cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster1"); + // route 1 + EXPECT_EQ(virtual_host.routes[1].matchers.path_matcher.ToString(), + "StringMatcher{prefix=}"); + action = absl::get_if( + &virtual_host.routes[1].action); + ASSERT_NE(action, nullptr); + cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster2"); + // route 2 + EXPECT_EQ(virtual_host.routes[2].matchers.path_matcher.ToString(), + "StringMatcher{safe_regex=/.*}"); + action = absl::get_if( + &virtual_host.routes[2].action); + ASSERT_NE(action, nullptr); + cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster3"); +} + +TEST_F(RouteMatchTest, PathMatchersInvalid) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_path_separated_prefix("foo"); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->mutable_safe_regex()->set_regex("["); + route_proto->mutable_route()->set_cluster("cluster2"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].match " + "error:invalid path specifier; " + "field:virtual_hosts[0].routes[1].match " + "error:error creating path matcher: " + "Invalid regex string specified in matcher: missing ]: []") + << decode_result.resource.status(); +} + +TEST_F(RouteMatchTest, HeaderMatchers) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + // header0: exact match with invert + auto* header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header0"); + header_proto->set_exact_match("exact1"); + header_proto->set_invert_match(true); + // header1: prefix match + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header1"); + header_proto->set_prefix_match("prefix1"); + // header2: suffix match + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header2"); + header_proto->set_suffix_match("suffix1"); + // header3: contains match + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header3"); + header_proto->set_contains_match("contains1"); + // header4: regex match + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header4"); + header_proto->mutable_safe_regex_match()->set_regex("regex1"); + // header5: range match + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header5"); + header_proto->mutable_range_match()->set_start(1); + header_proto->mutable_range_match()->set_end(3); + // header6: present match + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header6"); + header_proto->set_present_match(true); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + auto& virtual_host = resource.virtual_hosts.front(); + ASSERT_EQ(virtual_host.routes.size(), 1UL); + auto& header_matchers = virtual_host.routes[0].matchers.header_matchers; + ASSERT_EQ(header_matchers.size(), 7UL); + // header0: exact match with invert + EXPECT_EQ(header_matchers[0].ToString(), + "HeaderMatcher{header0 not StringMatcher{exact=exact1}}"); + // header1: prefix match + EXPECT_EQ(header_matchers[1].ToString(), + "HeaderMatcher{header1 StringMatcher{prefix=prefix1}}"); + // header2: suffix match + EXPECT_EQ(header_matchers[2].ToString(), + "HeaderMatcher{header2 StringMatcher{suffix=suffix1}}"); + // header3: contains match + EXPECT_EQ(header_matchers[3].ToString(), + "HeaderMatcher{header3 StringMatcher{contains=contains1}}"); + // header4: regex match + EXPECT_EQ(header_matchers[4].ToString(), + "HeaderMatcher{header4 StringMatcher{safe_regex=regex1}}"); + // header5: range match + EXPECT_EQ(header_matchers[5].ToString(), + "HeaderMatcher{header5 range=[1, 3]}"); + // header6: present match + EXPECT_EQ(header_matchers[6].ToString(), + "HeaderMatcher{header6 present=true}"); +} + +TEST_F(RouteMatchTest, HeaderMatchersInvalid) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + // header0: no match type set + auto* header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header0"); + // header1: range end before start + header_proto = route_proto->mutable_match()->add_headers(); + header_proto->set_name("header1"); + header_proto->mutable_range_match()->set_start(2); + header_proto->mutable_range_match()->set_end(1); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].match.headers[0] " + "error:invalid header matcher; " + "field:virtual_hosts[0].routes[0].match.headers[1] " + "error:cannot create header matcher: " + "Invalid range specifier specified: " + "end cannot be smaller than start.]") + << decode_result.resource.status(); +} + +TEST_F(RouteMatchTest, RuntimeFractionMatcher) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + // Route 0: 10 per 100 + auto* route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + auto* runtime_fraction_proto = route_proto->mutable_match() + ->mutable_runtime_fraction() + ->mutable_default_value(); + runtime_fraction_proto->set_numerator(10); + // Route 1: 10 per 10000 + route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + runtime_fraction_proto = route_proto->mutable_match() + ->mutable_runtime_fraction() + ->mutable_default_value(); + runtime_fraction_proto->set_numerator(10); + runtime_fraction_proto->set_denominator(runtime_fraction_proto->TEN_THOUSAND); + // Route 2: 10 per 1000000 + route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + runtime_fraction_proto = route_proto->mutable_match() + ->mutable_runtime_fraction() + ->mutable_default_value(); + runtime_fraction_proto->set_numerator(10); + runtime_fraction_proto->set_denominator(runtime_fraction_proto->MILLION); + // Route 3: runtime_fraction.default_value not set, so no fractional percent + route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_match()->mutable_runtime_fraction(); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + auto& virtual_host = resource.virtual_hosts.front(); + ASSERT_EQ(virtual_host.routes.size(), 4UL); + EXPECT_EQ(virtual_host.routes[0].matchers.fraction_per_million, 100000U); + EXPECT_EQ(virtual_host.routes[1].matchers.fraction_per_million, 1000U); + EXPECT_EQ(virtual_host.routes[2].matchers.fraction_per_million, 10U); + EXPECT_FALSE( + virtual_host.routes[3].matchers.fraction_per_million.has_value()); +} + +TEST_F(RouteMatchTest, RuntimeFractionMatcherInvalid) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_route()->set_cluster("cluster1"); + route_proto->mutable_match()->set_prefix(""); + auto* runtime_fraction_proto = route_proto->mutable_match() + ->mutable_runtime_fraction() + ->mutable_default_value(); + runtime_fraction_proto->set_numerator(10); + runtime_fraction_proto->set_denominator( + static_cast(5)); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].match.runtime_fraction" + ".default_value.denominator " + "error:unknown denominator type]") + << decode_result.resource.status(); +} + +// +// MaxStreamDuration tests +// + +using MaxStreamDurationTest = XdsRouteConfigTest; + +TEST_F(MaxStreamDurationTest, GrpcTimeoutHeaderMax) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + route_action_proto->mutable_max_stream_duration() + ->mutable_grpc_timeout_header_max() + ->set_seconds(3); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + EXPECT_EQ(action->max_stream_duration, Duration::Seconds(3)); +} + +TEST_F(MaxStreamDurationTest, MaxStreamDuration) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + route_action_proto->mutable_max_stream_duration() + ->mutable_max_stream_duration() + ->set_seconds(3); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + EXPECT_EQ(action->max_stream_duration, Duration::Seconds(3)); +} + +TEST_F(MaxStreamDurationTest, PrefersGrpcTimeoutHeaderMaxToMaxStreamDuration) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + route_action_proto->mutable_max_stream_duration() + ->mutable_grpc_timeout_header_max() + ->set_seconds(3); + route_action_proto->mutable_max_stream_duration() + ->mutable_max_stream_duration() + ->set_seconds(4); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + EXPECT_EQ(action->max_stream_duration, Duration::Seconds(3)); +} + +TEST_F(MaxStreamDurationTest, GrpcTimeoutHeaderMaxInvalid) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + route_action_proto->mutable_max_stream_duration() + ->mutable_grpc_timeout_header_max() + ->set_seconds(315576000001); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].route.max_stream_duration" + ".grpc_timeout_header_max.seconds " + "error:value must be in the range [0, 315576000000]]") + << decode_result.resource.status(); +} + +TEST_F(MaxStreamDurationTest, MaxStreamDurationInvalid) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + route_action_proto->mutable_max_stream_duration() + ->mutable_max_stream_duration() + ->set_seconds(315576000001); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].route.max_stream_duration" + ".max_stream_duration.seconds " + "error:value must be in the range [0, 315576000000]]") + << decode_result.resource.status(); +} + +// +// hash policy tests +// + +using HashPolicyTest = XdsRouteConfigTest; + +TEST_F(HashPolicyTest, ValidAndUnsupportedPolicies) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + // hash policy 0: header "header0" + auto* hash_policy_proto = route_action_proto->add_hash_policy(); + hash_policy_proto->mutable_header()->set_header_name("header0"); + // hash policy 1: header "header1" with regex_rewrite, terminal + hash_policy_proto = route_action_proto->add_hash_policy(); + auto* header_proto = hash_policy_proto->mutable_header(); + header_proto->set_header_name("header1"); + auto* regex_rewrite_proto = header_proto->mutable_regex_rewrite(); + regex_rewrite_proto->mutable_pattern()->set_regex(".*"); + regex_rewrite_proto->set_substitution("substitution"); + hash_policy_proto->set_terminal(true); + // hash policy 2: filter state "io.grpc.channel_id" + hash_policy_proto = route_action_proto->add_hash_policy(); + hash_policy_proto->mutable_filter_state()->set_key("io.grpc.channel_id"); + // hash policy 3: filter state with an unsupported key + hash_policy_proto = route_action_proto->add_hash_policy(); + hash_policy_proto->mutable_filter_state()->set_key("unsupported_key"); + // hash policy 4: cookie (unsupported) + route_action_proto->add_hash_policy()->mutable_cookie(); + // hash policy 5: connection_properties (unsupported) + route_action_proto->add_hash_policy()->mutable_connection_properties(); + // hash policy 6: query_parameter (unsupported) + route_action_proto->add_hash_policy()->mutable_query_parameter(); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto& hash_policies = action->hash_policies; + ASSERT_EQ(hash_policies.size(), 3UL); + // hash policy 0: header "header0" + auto* header = absl::get_if< + XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header>( + &hash_policies[0].policy); + ASSERT_NE(header, nullptr); + EXPECT_EQ(header->header_name, "header0"); + EXPECT_EQ(header->regex, nullptr); + EXPECT_EQ(header->regex_substitution, ""); + EXPECT_FALSE(hash_policies[0].terminal); + // hash policy 1: header "header1" with regex_rewrite + header = absl::get_if< + XdsRouteConfigResource::Route::RouteAction::HashPolicy::Header>( + &hash_policies[1].policy); + ASSERT_NE(header, nullptr); + EXPECT_EQ(header->header_name, "header1"); + ASSERT_NE(header->regex, nullptr); + EXPECT_EQ(header->regex->pattern(), ".*"); + EXPECT_EQ(header->regex_substitution, "substitution"); + EXPECT_TRUE(hash_policies[1].terminal); + // hash policy 2: filter state "io.grpc.channel_id", terminal + ASSERT_TRUE( + absl::holds_alternative< + XdsRouteConfigResource::Route::RouteAction::HashPolicy::ChannelId>( + hash_policies[2].policy)); + EXPECT_FALSE(hash_policies[2].terminal); +} + +TEST_F(HashPolicyTest, InvalidPolicies) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* route_action_proto = route_proto->mutable_route(); + route_action_proto->set_cluster("cluster1"); + // empty header name + route_action_proto->add_hash_policy()->mutable_header(); + // missing regex pattern + auto* header_proto = route_action_proto->add_hash_policy()->mutable_header(); + header_proto->set_header_name("header1"); + header_proto->mutable_regex_rewrite(); + // missing regex pattern string + header_proto = route_action_proto->add_hash_policy()->mutable_header(); + header_proto->set_header_name("header1"); + header_proto->mutable_regex_rewrite()->mutable_pattern(); + // invalid regex pattern string + header_proto = route_action_proto->add_hash_policy()->mutable_header(); + header_proto->set_header_name("header2"); + header_proto->mutable_regex_rewrite()->mutable_pattern()->set_regex("["); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].route.hash_policy[0].header" + ".header_name " + "error:must be non-empty; " + "field:virtual_hosts[0].routes[0].route.hash_policy[1].header" + ".regex_rewrite.pattern " + "error:field not present; " + "field:virtual_hosts[0].routes[0].route.hash_policy[2].header" + ".regex_rewrite.pattern.regex " + "error:field not present; " + "field:virtual_hosts[0].routes[0].route.hash_policy[3].header" + ".regex_rewrite.pattern.regex " + "error:errors compiling regex: missing ]: []") + << decode_result.resource.status(); +} + +// +// WeightedCluster tests +// + +using WeightedClusterTest = XdsRouteConfigTest; + +TEST_F(WeightedClusterTest, Basic) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* weighted_clusters_proto = + route_proto->mutable_route()->mutable_weighted_clusters(); + auto* cluster_weight_proto = weighted_clusters_proto->add_clusters(); + cluster_weight_proto->set_name("cluster1"); + cluster_weight_proto->mutable_weight()->set_value(123); + cluster_weight_proto = weighted_clusters_proto->add_clusters(); + cluster_weight_proto->set_name("cluster2"); + cluster_weight_proto->mutable_weight()->set_value(0); // Will be ignored. + cluster_weight_proto = weighted_clusters_proto->add_clusters(); + cluster_weight_proto->set_name("cluster3"); + cluster_weight_proto->mutable_weight()->set_value(456); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto* weighted_clusters = absl::get_if< + std::vector>( + &action->action); + ASSERT_NE(weighted_clusters, nullptr); + ASSERT_EQ(weighted_clusters->size(), 2UL); + EXPECT_EQ((*weighted_clusters)[0].name, "cluster1"); + EXPECT_EQ((*weighted_clusters)[0].weight, 123); + EXPECT_EQ((*weighted_clusters)[1].name, "cluster3"); + EXPECT_EQ((*weighted_clusters)[1].weight, 456); +} + +TEST_F(WeightedClusterTest, Invalid) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + auto* weighted_clusters_proto = + route_proto->mutable_route()->mutable_weighted_clusters(); + // Empty cluster name. + auto* cluster_weight_proto = weighted_clusters_proto->add_clusters(); + cluster_weight_proto->set_name(""); + cluster_weight_proto->mutable_weight()->set_value(123); + // Weight not present. + weighted_clusters_proto->add_clusters()->set_name("cluster1"); + // Second route has only a cluster with 0 weight. + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + cluster_weight_proto = + route_proto->mutable_route()->mutable_weighted_clusters()->add_clusters(); + cluster_weight_proto->set_name("cluster1"); + cluster_weight_proto->mutable_weight()->set_value(0); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].route.weighted_clusters" + ".clusters[0].name " + "error:must be non-empty; " + "field:virtual_hosts[0].routes[0].route.weighted_clusters" + ".clusters[1].weight " + "error:field not present; " + "field:virtual_hosts[0].routes[1].route.weighted_clusters " + "error:no valid clusters specified]") + << decode_result.resource.status(); +} + +// +// RLS tests +// + +using RlsTest = XdsRouteConfigTest; + +TEST_F(RlsTest, Basic) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + RouteLookupClusterSpecifier rls_cluster_specifier; + auto* rls_config = rls_cluster_specifier.mutable_route_lookup_config(); + rls_config->set_cache_size_bytes(1024); + rls_config->set_lookup_service("rls.example.com"); + auto* grpc_keybuilder = rls_config->add_grpc_keybuilders(); + grpc_keybuilder->add_names()->set_service("service"); + typed_extension_config->mutable_typed_config()->PackFrom( + rls_cluster_specifier); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT( + resource.cluster_specifier_plugin_map, + ::testing::ElementsAre(::testing::Pair( + "rls", + "[{\"rls_experimental\":{" + "\"childPolicy\":[{\"cds_experimental\":{}}]," + "\"childPolicyConfigTargetFieldName\":\"cluster\"," + "\"routeLookupConfig\":{" + "\"cacheSizeBytes\":\"1024\"," + "\"grpcKeybuilders\":[{\"names\":[{\"service\":\"service\"}]}]," + "\"lookupService\":\"rls.example.com\"}}}]"))); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto* plugin_name = absl::get_if< + XdsRouteConfigResource::Route::RouteAction::ClusterSpecifierPluginName>( + &action->action); + ASSERT_NE(plugin_name, nullptr); + EXPECT_EQ(plugin_name->cluster_specifier_plugin_name, "rls"); +} + +TEST_F(RlsTest, PluginDefinedButNotUsed) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + RouteLookupClusterSpecifier rls_cluster_specifier; + auto* rls_config = rls_cluster_specifier.mutable_route_lookup_config(); + rls_config->set_cache_size_bytes(1024); + rls_config->set_lookup_service("rls.example.com"); + auto* grpc_keybuilder = rls_config->add_grpc_keybuilders(); + grpc_keybuilder->add_names()->set_service("service"); + typed_extension_config->mutable_typed_config()->PackFrom( + rls_cluster_specifier); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT(resource.cluster_specifier_plugin_map, ::testing::ElementsAre()); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto* cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster1"); +} + +TEST_F(RlsTest, NotUsedInAllVirtualHosts) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + RouteLookupClusterSpecifier rls_cluster_specifier; + auto* rls_config = rls_cluster_specifier.mutable_route_lookup_config(); + rls_config->set_cache_size_bytes(1024); + rls_config->set_lookup_service("rls.example.com"); + auto* grpc_keybuilder = rls_config->add_grpc_keybuilders(); + grpc_keybuilder->add_names()->set_service("service"); + typed_extension_config->mutable_typed_config()->PackFrom( + rls_cluster_specifier); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + vhost = route_config.add_virtual_hosts(); + vhost->add_domains("foo.example.com"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT( + resource.cluster_specifier_plugin_map, + ::testing::ElementsAre(::testing::Pair( + "rls", + "[{\"rls_experimental\":{" + "\"childPolicy\":[{\"cds_experimental\":{}}]," + "\"childPolicyConfigTargetFieldName\":\"cluster\"," + "\"routeLookupConfig\":{" + "\"cacheSizeBytes\":\"1024\"," + "\"grpcKeybuilders\":[{\"names\":[{\"service\":\"service\"}]}]," + "\"lookupService\":\"rls.example.com\"}}}]"))); + ASSERT_EQ(resource.virtual_hosts.size(), 2UL); + EXPECT_THAT(resource.virtual_hosts[0].domains, ::testing::ElementsAre("*")); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto* route = &resource.virtual_hosts[0].routes[0]; + auto* action = + absl::get_if(&route->action); + ASSERT_NE(action, nullptr); + auto* plugin_name = absl::get_if< + XdsRouteConfigResource::Route::RouteAction::ClusterSpecifierPluginName>( + &action->action); + ASSERT_NE(plugin_name, nullptr); + EXPECT_EQ(plugin_name->cluster_specifier_plugin_name, "rls"); + EXPECT_THAT(resource.virtual_hosts[1].domains, + ::testing::ElementsAre("foo.example.com")); + ASSERT_EQ(resource.virtual_hosts[1].routes.size(), 1UL); + route = &resource.virtual_hosts[1].routes[0]; + action = + absl::get_if(&route->action); + ASSERT_NE(action, nullptr); + auto* cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster1"); +} + +TEST_F(RlsTest, ClusterSpecifierPluginsIgnoredWhenNotEnabled) { + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + RouteLookupClusterSpecifier rls_cluster_specifier; + auto* rls_config = rls_cluster_specifier.mutable_route_lookup_config(); + rls_config->set_cache_size_bytes(1024); + rls_config->set_lookup_service("rls.example.com"); + auto* grpc_keybuilder = rls_config->add_grpc_keybuilders(); + grpc_keybuilder->add_names()->set_service("service"); + typed_extension_config->mutable_typed_config()->PackFrom( + rls_cluster_specifier); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT(resource.cluster_specifier_plugin_map, ::testing::ElementsAre()); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto& matchers = route.matchers; + EXPECT_EQ(matchers.path_matcher.ToString(), "StringMatcher{prefix=}"); + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto* cluster_name = + absl::get_if( + &action->action); + ASSERT_NE(cluster_name, nullptr); + EXPECT_EQ(cluster_name->cluster_name, "cluster1"); +} + +TEST_F(RlsTest, DuplicateClusterSpecifierPluginNames) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + RouteLookupClusterSpecifier rls_cluster_specifier; + auto* rls_config = rls_cluster_specifier.mutable_route_lookup_config(); + rls_config->set_cache_size_bytes(1024); + rls_config->set_lookup_service("rls.example.com"); + auto* grpc_keybuilder = rls_config->add_grpc_keybuilders(); + grpc_keybuilder->add_names()->set_service("service"); + typed_extension_config->mutable_typed_config()->PackFrom( + rls_cluster_specifier); + *route_config.add_cluster_specifier_plugins() = *cluster_specifier_plugin; + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[1].extension.name " + "error:duplicate name \"rls\"]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, ClusterSpecifierPluginTypedConfigNotPresent) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[0].extension.typed_config " + "error:field not present]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, UnsupportedClusterSpecifierPlugin) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + typed_extension_config->mutable_typed_config()->PackFrom( + RouteConfiguration()); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[0].extension.typed_config.value[" + "envoy.config.route.v3.RouteConfiguration] " + "error:unsupported ClusterSpecifierPlugin type]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, UnsupportedButOptionalClusterSpecifierPlugin) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + cluster_specifier_plugin->set_is_optional(true); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + typed_extension_config->mutable_typed_config()->PackFrom( + RouteConfiguration()); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster("cluster1"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + ASSERT_TRUE(decode_result.resource.ok()) << decode_result.resource.status(); + ASSERT_TRUE(decode_result.name.has_value()); + EXPECT_EQ(*decode_result.name, "foo"); + auto& resource = + static_cast(**decode_result.resource); + EXPECT_THAT(resource.cluster_specifier_plugin_map, ::testing::ElementsAre()); + ASSERT_EQ(resource.virtual_hosts.size(), 1UL); + ASSERT_EQ(resource.virtual_hosts[0].routes.size(), 1UL); + auto& route = resource.virtual_hosts[0].routes[0]; + auto& matchers = route.matchers; + EXPECT_EQ(matchers.path_matcher.ToString(), "StringMatcher{prefix=}"); + auto* action = + absl::get_if(&route.action); + ASSERT_NE(action, nullptr); + auto* cluster = + absl::get_if( + &action->action); + ASSERT_NE(cluster, nullptr); + EXPECT_EQ(cluster->cluster_name, "cluster1"); +} + +TEST_F(RlsTest, InvalidGrpcLbPolicyConfig) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + RouteLookupClusterSpecifier rls_cluster_specifier; + auto* rls_config = rls_cluster_specifier.mutable_route_lookup_config(); + rls_config->set_cache_size_bytes(1024); + auto* grpc_keybuilder = rls_config->add_grpc_keybuilders(); + grpc_keybuilder->add_names()->set_service("service"); + typed_extension_config->mutable_typed_config()->PackFrom( + rls_cluster_specifier); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[0].extension.typed_config " + "error:ClusterSpecifierPlugin returned invalid LB policy config: " + "errors validing RLS LB policy config: [" + "field:routeLookupConfig.lookupService error:field not present]]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, RlsInTypedStruct) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + xds::type::v3::TypedStruct typed_struct; + typed_struct.set_type_url( + "types.googleapis.com/grpc.lookup.v1.RouteLookupClusterSpecifier"); + typed_extension_config->mutable_typed_config()->PackFrom(typed_struct); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[0].extension.typed_config.value[" + "xds.type.v3.TypedStruct].value[" + "grpc.lookup.v1.RouteLookupClusterSpecifier] " + "error:could not parse plugin config]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, RlsConfigUnparseable) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + auto* typed_config = typed_extension_config->mutable_typed_config(); + typed_config->PackFrom(RouteLookupClusterSpecifier()); + typed_config->set_value(std::string("\0", 1)); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[0].extension.typed_config.value[" + "grpc.lookup.v1.RouteLookupClusterSpecifier] " + "error:could not parse plugin config]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, RlsMissingRouteLookupConfig) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* cluster_specifier_plugin = route_config.add_cluster_specifier_plugins(); + auto* typed_extension_config = cluster_specifier_plugin->mutable_extension(); + typed_extension_config->set_name("rls"); + typed_extension_config->mutable_typed_config()->PackFrom( + RouteLookupClusterSpecifier()); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:cluster_specifier_plugins[0].extension.typed_config.value[" + "grpc.lookup.v1.RouteLookupClusterSpecifier].route_lookup_config " + "error:field not present]") + << decode_result.resource.status(); +} + +TEST_F(RlsTest, RouteUsesUnconfiguredClusterSpecifierPlugin) { + ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); + RouteConfiguration route_config; + route_config.set_name("foo"); + auto* vhost = route_config.add_virtual_hosts(); + vhost->add_domains("*"); + auto* route_proto = vhost->add_routes(); + route_proto->mutable_match()->set_prefix(""); + route_proto->mutable_route()->set_cluster_specifier_plugin("rls"); + std::string serialized_resource; + ASSERT_TRUE(route_config.SerializeToString(&serialized_resource)); + auto* resource_type = XdsRouteConfigResourceType::Get(); + auto decode_result = + resource_type->Decode(decode_context_, serialized_resource); + EXPECT_EQ(decode_result.resource.status().code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(decode_result.resource.status().message(), + "errors validating RouteConfiguration resource: [" + "field:virtual_hosts[0].routes[0].route.cluster_specifier_plugin " + "error:unknown cluster specifier plugin name \"rls\"]") + << decode_result.resource.status(); +} + +} // namespace +} // namespace testing +} // namespace grpc_core + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + grpc::testing::TestEnvironment env(&argc, argv); + grpc_init(); + int ret = RUN_ALL_TESTS(); + grpc_shutdown(); + return ret; +} diff --git a/test/cpp/end2end/xds/xds_csds_end2end_test.cc b/test/cpp/end2end/xds/xds_csds_end2end_test.cc index 7401f40e9cc..554c0742b20 100644 --- a/test/cpp/end2end/xds/xds_csds_end2end_test.cc +++ b/test/cpp/end2end/xds/xds_csds_end2end_test.cc @@ -467,7 +467,9 @@ TEST_P(ClientStatusDiscoveryServiceTest, XdsConfigDumpRouteError) { kDefaultRouteConfigurationName, kDefaultClusterName)), ClientResourceStatus::NACKED, EqUpdateFailureState( - ::testing::HasSubstr("VirtualHost has no domains"), "2")))); + ::testing::HasSubstr( + "field:virtual_hosts[0].domains error:must be non-empty"), + "2")))); } else { ok = ::testing::Value( csds_response.config(0).generic_xds_configs(), @@ -478,7 +480,12 @@ TEST_P(ClientStatusDiscoveryServiceTest, XdsConfigDumpRouteError) { kDefaultClusterName))), ClientResourceStatus::NACKED, EqUpdateFailureState( - ::testing::HasSubstr("VirtualHost has no domains"), "2")))); + ::testing::HasSubstr( + "field:api_listener.api_listener.value[envoy.extensions" + ".filters.network.http_connection_manager.v3" + ".HttpConnectionManager].route_config.virtual_hosts[0]" + ".domains error:must be non-empty"), + "2")))); } if (ok) return; // TEST PASSED! gpr_sleep_until( diff --git a/test/cpp/end2end/xds/xds_end2end_test.cc b/test/cpp/end2end/xds/xds_end2end_test.cc index 6ccc2c294e7..a96fd3bd5d6 100644 --- a/test/cpp/end2end/xds/xds_end2end_test.cc +++ b/test/cpp/end2end/xds/xds_end2end_test.cc @@ -1796,62 +1796,6 @@ TEST_P(XdsServerRdsTest, Basic) { SendRpc([this]() { return CreateInsecureChannel(); }, {}, {}); } -TEST_P(XdsServerRdsTest, NacksInvalidDomainPattern) { - RouteConfiguration route_config = default_server_route_config_; - route_config.mutable_virtual_hosts()->at(0).add_domains(""); - SetServerListenerNameAndRouteConfiguration( - balancer_.get(), default_server_listener_, backends_[0]->port(), - route_config); - backends_[0]->Start(); - const auto response_state = WaitForRouteConfigNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("Invalid domain pattern \"\"")); -} - -TEST_P(XdsServerRdsTest, NacksEmptyDomainsList) { - RouteConfiguration route_config = default_server_route_config_; - route_config.mutable_virtual_hosts()->at(0).clear_domains(); - SetServerListenerNameAndRouteConfiguration( - balancer_.get(), default_server_listener_, backends_[0]->port(), - route_config); - backends_[0]->Start(); - const auto response_state = WaitForRouteConfigNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("VirtualHost has no domains")); -} - -TEST_P(XdsServerRdsTest, NacksEmptyRoutesList) { - RouteConfiguration route_config = default_server_route_config_; - route_config.mutable_virtual_hosts()->at(0).clear_routes(); - SetServerListenerNameAndRouteConfiguration( - balancer_.get(), default_server_listener_, backends_[0]->port(), - route_config); - backends_[0]->Start(); - const auto response_state = WaitForRouteConfigNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No route found in the virtual host")); -} - -TEST_P(XdsServerRdsTest, NacksEmptyMatch) { - RouteConfiguration route_config = default_server_route_config_; - route_config.mutable_virtual_hosts() - ->at(0) - .mutable_routes() - ->at(0) - .clear_match(); - SetServerListenerNameAndRouteConfiguration( - balancer_.get(), default_server_listener_, backends_[0]->port(), - route_config); - backends_[0]->Start(); - const auto response_state = WaitForRouteConfigNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("Match can't be null")); -} - TEST_P(XdsServerRdsTest, FailsRouteMatchesOtherThanNonForwardingAction) { SetServerListenerNameAndRouteConfiguration( balancer_.get(), default_server_listener_, backends_[0]->port(), diff --git a/test/cpp/end2end/xds/xds_rls_end2end_test.cc b/test/cpp/end2end/xds/xds_rls_end2end_test.cc index 239ff8937c3..95cfdacfbcc 100644 --- a/test/cpp/end2end/xds/xds_rls_end2end_test.cc +++ b/test/cpp/end2end/xds/xds_rls_end2end_test.cc @@ -161,224 +161,6 @@ TEST_P(RlsTest, XdsRoutingClusterSpecifierPlugin) { EXPECT_EQ(kNumEchoRpcs, backends_[1]->backend_service()->request_count()); } -TEST_P(RlsTest, XdsRoutingClusterSpecifierPluginNotUsedInAllVhosts) { - ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); - CreateAndStartBackends(2); - const char* kNewClusterName = "new_cluster"; - const char* kNewEdsServiceName = "new_eds_service_name"; - const size_t kNumEchoRpcs = 5; - // Populate new EDS resources. - EdsResourceArgs args({ - {"locality0", CreateEndpointsForBackends(0, 1)}, - }); - EdsResourceArgs args1({ - {"locality0", CreateEndpointsForBackends(1, 2)}, - }); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - balancer_->ads_service()->SetEdsResource( - BuildEdsResource(args1, kNewEdsServiceName)); - // Populate new CDS resources. - Cluster new_cluster = default_cluster_; - new_cluster.set_name(kNewClusterName); - new_cluster.mutable_eds_cluster_config()->set_service_name( - kNewEdsServiceName); - balancer_->ads_service()->SetCdsResource(new_cluster); - // Prepare the RLSLookupConfig and configure all the keys; change route - // configurations to use cluster specifier plugin. - rls_server_->rls_service()->SetResponse( - BuildRlsRequest({{kRlsTestKey, kRlsTestValue}, - {kRlsHostKey, kServerName}, - {kRlsServiceKey, kRlsServiceValue}, - {kRlsMethodKey, kRlsMethodValue}, - {kRlsConstantKey, kRlsConstantValue}}), - BuildRlsResponse({kNewClusterName})); - RouteLookupConfig route_lookup_config; - auto* key_builder = route_lookup_config.add_grpc_keybuilders(); - auto* name = key_builder->add_names(); - name->set_service(kRlsServiceValue); - name->set_method(kRlsMethodValue); - auto* header = key_builder->add_headers(); - header->set_key(kRlsTestKey); - header->add_names(kRlsTestKey1); - header->add_names("key2"); - auto* extra_keys = key_builder->mutable_extra_keys(); - extra_keys->set_host(kRlsHostKey); - extra_keys->set_service(kRlsServiceKey); - extra_keys->set_method(kRlsMethodKey); - (*key_builder->mutable_constant_keys())[kRlsConstantKey] = kRlsConstantValue; - route_lookup_config.set_lookup_service( - absl::StrCat("localhost:", rls_server_->port())); - route_lookup_config.set_cache_size_bytes(5000); - RouteLookupClusterSpecifier rls; - *rls.mutable_route_lookup_config() = std::move(route_lookup_config); - RouteConfiguration new_route_config = default_route_config_; - auto* plugin = new_route_config.add_cluster_specifier_plugins(); - plugin->mutable_extension()->set_name(kRlsClusterSpecifierPluginInstanceName); - plugin->mutable_extension()->mutable_typed_config()->PackFrom(rls); - // Duplicate the virtual host, but with a different domain. - *new_route_config.add_virtual_hosts() = new_route_config.virtual_hosts(0); - new_route_config.mutable_virtual_hosts(1)->clear_domains(); - new_route_config.mutable_virtual_hosts(1)->add_domains("www.example.com"); - // In the original virtual host, set the default route to use the RLS plugin. - auto* default_route = - new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - default_route->mutable_route()->set_cluster_specifier_plugin( - kRlsClusterSpecifierPluginInstanceName); - SetRouteConfiguration(balancer_.get(), new_route_config); - auto rpc_options = RpcOptions().set_metadata({{kRlsTestKey1, kRlsTestValue}}); - WaitForAllBackends(DEBUG_LOCATION, 1, 2, /*check_status=*/nullptr, - WaitForBackendOptions(), rpc_options); - CheckRpcSendOk(DEBUG_LOCATION, kNumEchoRpcs, rpc_options); - // Make sure RPCs all go to the correct backend. - EXPECT_EQ(kNumEchoRpcs, backends_[1]->backend_service()->request_count()); -} - -TEST_P(RlsTest, XdsRoutingClusterSpecifierPluginNacksUndefinedSpecifier) { - ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); - RouteConfiguration new_route_config = default_route_config_; - auto* default_route = - new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - // Set Cluster Specifier Plugin to something that does not exist. - default_route->mutable_route()->set_cluster_specifier_plugin( - kRlsClusterSpecifierPluginInstanceName); - SetRouteConfiguration(balancer_.get(), new_route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr(absl::StrCat( - "RouteAction cluster contains cluster specifier plugin " - "name not configured: ", - kRlsClusterSpecifierPluginInstanceName))); -} - -TEST_P(RlsTest, XdsRoutingClusterSpecifierPluginNacksDuplicateSpecifier) { - ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); - // Prepare the RLSLookupConfig: change route configurations to use cluster - // specifier plugin. - RouteLookupConfig route_lookup_config; - auto* key_builder = route_lookup_config.add_grpc_keybuilders(); - auto* name = key_builder->add_names(); - name->set_service(kRlsServiceValue); - name->set_method(kRlsMethodValue); - auto* header = key_builder->add_headers(); - header->set_key(kRlsTestKey); - header->add_names(kRlsTestKey1); - route_lookup_config.set_lookup_service( - absl::StrCat("localhost:", rls_server_->port())); - route_lookup_config.set_cache_size_bytes(5000); - RouteLookupClusterSpecifier rls; - *rls.mutable_route_lookup_config() = std::move(route_lookup_config); - RouteConfiguration new_route_config = default_route_config_; - auto* plugin = new_route_config.add_cluster_specifier_plugins(); - plugin->mutable_extension()->set_name(kRlsClusterSpecifierPluginInstanceName); - plugin->mutable_extension()->mutable_typed_config()->PackFrom(rls); - auto* duplicate_plugin = new_route_config.add_cluster_specifier_plugins(); - duplicate_plugin->mutable_extension()->set_name( - kRlsClusterSpecifierPluginInstanceName); - duplicate_plugin->mutable_extension()->mutable_typed_config()->PackFrom(rls); - auto* default_route = - new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - default_route->mutable_route()->set_cluster_specifier_plugin( - kRlsClusterSpecifierPluginInstanceName); - SetRouteConfiguration(balancer_.get(), new_route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr(absl::StrCat( - "Duplicated definition of cluster_specifier_plugin ", - kRlsClusterSpecifierPluginInstanceName))); -} - -TEST_P(RlsTest, - XdsRoutingClusterSpecifierPluginNacksUnknownSpecifierProtoNotOptional) { - ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); - // Prepare the RLSLookupConfig: change route configurations to use cluster - // specifier plugin. - RouteLookupConfig route_lookup_config; - RouteConfiguration new_route_config = default_route_config_; - auto* plugin = new_route_config.add_cluster_specifier_plugins(); - plugin->mutable_extension()->set_name(kRlsClusterSpecifierPluginInstanceName); - // Instead of grpc.lookup.v1.RouteLookupClusterSpecifier, let's say we - // mistakenly packed the inner RouteLookupConfig instead. - plugin->mutable_extension()->mutable_typed_config()->PackFrom( - route_lookup_config); - auto* default_route = - new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - default_route->mutable_route()->set_cluster_specifier_plugin( - kRlsClusterSpecifierPluginInstanceName); - SetRouteConfiguration(balancer_.get(), new_route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("Unknown ClusterSpecifierPlugin type " - "grpc.lookup.v1.RouteLookupConfig")); -} - -TEST_P(RlsTest, - XdsRoutingClusterSpecifierPluginIgnoreUnknownSpecifierProtoOptional) { - ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); - CreateAndStartBackends(1); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - // Prepare the RLSLookupConfig: change route configurations to use cluster - // specifier plugin. - RouteLookupConfig route_lookup_config; - RouteConfiguration new_route_config = default_route_config_; - auto* plugin = new_route_config.add_cluster_specifier_plugins(); - plugin->mutable_extension()->set_name(kRlsClusterSpecifierPluginInstanceName); - // Instead of grpc.lookup.v1.RouteLookupClusterSpecifier, let's say we - // mistakenly packed the inner RouteLookupConfig instead. - plugin->mutable_extension()->mutable_typed_config()->PackFrom( - route_lookup_config); - plugin->set_is_optional(true); - auto* route = new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route->mutable_route()->set_cluster_specifier_plugin( - kRlsClusterSpecifierPluginInstanceName); - auto* default_route = new_route_config.mutable_virtual_hosts(0)->add_routes(); - default_route->mutable_match()->set_prefix(""); - default_route->mutable_route()->set_cluster(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), new_route_config); - // Ensure we ignore the cluster specifier plugin and send traffic according to - // the default route. - WaitForAllBackends(DEBUG_LOCATION); -} - -TEST_P(RlsTest, XdsRoutingRlsClusterSpecifierPluginNacksRequiredMatch) { - ScopedExperimentalEnvVar env_var("GRPC_EXPERIMENTAL_XDS_RLS_LB"); - // Prepare the RLSLookupConfig and configure all the keys; add required_match - // field which should not be there. - RouteLookupConfig route_lookup_config; - auto* key_builder = route_lookup_config.add_grpc_keybuilders(); - auto* name = key_builder->add_names(); - name->set_service(kRlsServiceValue); - name->set_method(kRlsMethodValue); - auto* header = key_builder->add_headers(); - header->set_key(kRlsTestKey); - header->add_names(kRlsTestKey1); - header->set_required_match(true); - route_lookup_config.set_lookup_service( - absl::StrCat("localhost:", rls_server_->port())); - route_lookup_config.set_cache_size_bytes(5000); - RouteLookupClusterSpecifier rls; - *rls.mutable_route_lookup_config() = std::move(route_lookup_config); - RouteConfiguration new_route_config = default_route_config_; - auto* plugin = new_route_config.add_cluster_specifier_plugins(); - plugin->mutable_extension()->set_name(kRlsClusterSpecifierPluginInstanceName); - plugin->mutable_extension()->mutable_typed_config()->PackFrom(rls); - auto* default_route = - new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - default_route->mutable_route()->set_cluster_specifier_plugin( - kRlsClusterSpecifierPluginInstanceName); - SetRouteConfiguration(balancer_.get(), new_route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr( - "field:routeLookupConfig.grpcKeybuilders[0].headers[0].requiredMatch " - "error:must not be present")); -} - TEST_P(RlsTest, XdsRoutingClusterSpecifierPluginDisabled) { CreateAndStartBackends(1); // Populate new EDS resources. diff --git a/test/cpp/end2end/xds/xds_routing_end2end_test.cc b/test/cpp/end2end/xds/xds_routing_end2end_test.cc index dff0c9bf5b8..aad1224e47b 100644 --- a/test/cpp/end2end/xds/xds_routing_end2end_test.cc +++ b/test/cpp/end2end/xds/xds_routing_end2end_test.cc @@ -148,21 +148,6 @@ MATCHER_P2(AdjustedClockInRange, t1, t2, "equals time") { return ok; } -TEST_P(LdsRdsTest, DefaultRouteSpecifiesSlashPrefix) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_match() - ->set_prefix("/"); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - // We need to wait for all backends to come online. - WaitForAllBackends(DEBUG_LOCATION); -} - // Tests that LDS client ACKs but fails if matching domain can't be found in // the LDS response. TEST_P(LdsRdsTest, NoMatchedDomain) { @@ -231,165 +216,31 @@ TEST_P(LdsRdsTest, NoMatchingRoute) { EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); } -// Tests that LDS client should ignore route which has query_parameters. -TEST_P(LdsRdsTest, RouteMatchHasQueryParameters) { - RouteConfiguration route_config = 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()->add_query_parameters(); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// 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) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - 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(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), route_config); - (void)SendRpc(); - const auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Tests that LDS client should ignore route which has a path -// prefix string does not start with "/". -TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixNoLeadingSlash) { +// Testing just one example of an invalid resource here. +// Unit tests for XdsRouteConfigResourceType have exhaustive tests for all +// of the invalid cases. +TEST_P(LdsRdsTest, NacksInvalidRouteConfig) { RouteConfiguration route_config = default_route_config_; auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); route1->mutable_match()->set_prefix("grpc.testing.EchoTest1Service/"); SetRouteConfiguration(balancer_.get(), route_config); const auto response_state = WaitForRdsNack(DEBUG_LOCATION); ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has a prefix -// string with more than 2 slashes. -TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixExtraContent) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/Echo1/"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has a prefix -// string "//". -TEST_P(LdsRdsTest, RouteMatchHasInvalidPrefixDoubleSlash) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("//"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has path -// but it's empty. -TEST_P(LdsRdsTest, RouteMatchHasInvalidPathEmptyPath) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_path(""); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has path -// string does not start with "/". -TEST_P(LdsRdsTest, RouteMatchHasInvalidPathNoLeadingSlash) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_path("grpc.testing.EchoTest1Service/Echo1"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has path -// string that has too many slashes; for example, ends with "/". -TEST_P(LdsRdsTest, RouteMatchHasInvalidPathTooManySlashes) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_path("/grpc.testing.EchoTest1Service/Echo1/"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// 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) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_path("/grpc.testing.EchoTest1Service.Echo1"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has path -// string that is missing service. -TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingService) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_path("//Echo1"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Tests that LDS client should ignore route which has path -// string that is missing method. -TEST_P(LdsRdsTest, RouteMatchHasInvalidPathMissingMethod) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_path("/grpc.testing.EchoTest1Service/"); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("No valid routes specified.")); -} - -// Test that LDS client should reject route which has invalid path regex. -TEST_P(LdsRdsTest, RouteMatchHasInvalidPathRegex) { - const char* kNewCluster1Name = "new_cluster_1"; - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->mutable_safe_regex()->set_regex("a[z-a]"); - route1->mutable_route()->set_cluster(kNewCluster1Name); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "path matcher: Invalid regex string specified in matcher.")); + EXPECT_EQ( + response_state->error_message, + absl::StrCat( + "xDS response validation errors: [resource index 0: ", + GetParam().enable_rds_testing() + ? "route_config_name: INVALID_ARGUMENT: " + "errors validating RouteConfiguration resource: [" + "field:" + : "server.example.com: INVALID_ARGUMENT: " + "errors validating ApiListener: [" + "field:api_listener.api_listener.value[" + "envoy.extensions.filters.network.http_connection_manager.v3" + ".HttpConnectionManager].route_config.", + "virtual_hosts[0].routes " + "error:no valid routes in VirtualHost]]")); } // Tests that LDS client should fail RPCs with UNAVAILABLE status code if the @@ -409,158 +260,6 @@ TEST_P(LdsRdsTest, MatchingRouteHasNoRouteAction) { "Matching route has inappropriate action"); } -TEST_P(LdsRdsTest, RouteActionClusterHasEmptyClusterName) { - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/"); - route1->mutable_route()->set_cluster(""); - auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes(); - default_route->mutable_match()->set_prefix(""); - default_route->mutable_route()->set_cluster(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr("RouteAction cluster contains empty cluster name.")); -} - -TEST_P(LdsRdsTest, RouteActionWeightedTargetHasIncorrectTotalWeightSet) { - const size_t kWeight75 = 75; - const char* kNewCluster1Name = "new_cluster_1"; - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/"); - auto* weighted_cluster1 = - route1->mutable_route()->mutable_weighted_clusters()->add_clusters(); - weighted_cluster1->set_name(kNewCluster1Name); - weighted_cluster1->mutable_weight()->set_value(kWeight75); - route1->mutable_route() - ->mutable_weighted_clusters() - ->mutable_total_weight() - ->set_value(kWeight75 + 1); - auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes(); - default_route->mutable_match()->set_prefix(""); - default_route->mutable_route()->set_cluster(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "RouteAction weighted_cluster has incorrect total weight")); -} - -TEST_P(LdsRdsTest, RouteActionWeightedClusterHasZeroTotalWeight) { - const char* kNewCluster1Name = "new_cluster_1"; - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/"); - auto* weighted_cluster1 = - route1->mutable_route()->mutable_weighted_clusters()->add_clusters(); - weighted_cluster1->set_name(kNewCluster1Name); - weighted_cluster1->mutable_weight()->set_value(0); - route1->mutable_route() - ->mutable_weighted_clusters() - ->mutable_total_weight() - ->set_value(0); - auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes(); - default_route->mutable_match()->set_prefix(""); - default_route->mutable_route()->set_cluster(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr( - "RouteAction weighted_cluster has no valid clusters specified.")); -} - -TEST_P(LdsRdsTest, RouteActionWeightedTargetClusterHasEmptyClusterName) { - const size_t kWeight75 = 75; - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/"); - auto* weighted_cluster1 = - route1->mutable_route()->mutable_weighted_clusters()->add_clusters(); - weighted_cluster1->set_name(""); - weighted_cluster1->mutable_weight()->set_value(kWeight75); - route1->mutable_route() - ->mutable_weighted_clusters() - ->mutable_total_weight() - ->set_value(kWeight75); - auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes(); - default_route->mutable_match()->set_prefix(""); - default_route->mutable_route()->set_cluster(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("RouteAction weighted_cluster cluster " - "contains empty cluster name.")); -} - -TEST_P(LdsRdsTest, RouteActionWeightedTargetClusterHasNoWeight) { - const size_t kWeight75 = 75; - const char* kNewCluster1Name = "new_cluster_1"; - RouteConfiguration route_config = default_route_config_; - auto* route1 = route_config.mutable_virtual_hosts(0)->mutable_routes(0); - route1->mutable_match()->set_prefix("/grpc.testing.EchoTest1Service/"); - auto* weighted_cluster1 = - route1->mutable_route()->mutable_weighted_clusters()->add_clusters(); - weighted_cluster1->set_name(kNewCluster1Name); - route1->mutable_route() - ->mutable_weighted_clusters() - ->mutable_total_weight() - ->set_value(kWeight75); - auto* default_route = route_config.mutable_virtual_hosts(0)->add_routes(); - default_route->mutable_match()->set_prefix(""); - default_route->mutable_route()->set_cluster(kDefaultClusterName); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "RouteAction weighted_cluster cluster missing weight")); -} - -TEST_P(LdsRdsTest, RouteHeaderMatchInvalidRegex) { - const char* kNewCluster1Name = "new_cluster_1"; - RouteConfiguration route_config = 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_safe_regex_match()->set_regex("a[z-a]"); - route1->mutable_route()->set_cluster(kNewCluster1Name); - SetRouteConfiguration(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr( - "header matcher: Invalid regex string specified in matcher.")); -} - -TEST_P(LdsRdsTest, RouteHeaderMatchInvalidRange) { - const char* kNewCluster1Name = "new_cluster_1"; - RouteConfiguration route_config = 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(balancer_.get(), route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr( - "header matcher: Invalid range specifier specified: end cannot be " - "smaller than start.")); -} - // Tests that LDS client should choose the default route (with no matching // specified) after unable to find a match with previous routes. TEST_P(LdsRdsTest, XdsRoutingPathMatching) { @@ -1942,55 +1641,6 @@ TEST_P(LdsRdsTest, EXPECT_EQ(1, backends_[0]->backend_service()->request_count()); } -TEST_P(LdsRdsTest, XdsRetryPolicyInvalidNumRetriesZero) { - CreateAndStartBackends(1); - // Populate new EDS resources. - EdsResourceArgs args({ - {"locality0", CreateEndpointsForBackends(0, 1)}, - }); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - // Construct route config to set retry policy. - RouteConfiguration new_route_config = default_route_config_; - auto* route1 = new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - auto* retry_policy = route1->mutable_route()->mutable_retry_policy(); - retry_policy->set_retry_on("deadline-exceeded"); - // Setting num_retries to zero is not valid. - retry_policy->mutable_num_retries()->set_value(0); - SetRouteConfiguration(balancer_.get(), new_route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr( - "RouteAction RetryPolicy num_retries set to invalid value 0.")); -} - -TEST_P(LdsRdsTest, XdsRetryPolicyRetryBackOffMissingBaseInterval) { - CreateAndStartBackends(1); - // Populate new EDS resources. - EdsResourceArgs args({ - {"locality0", CreateEndpointsForBackends(0, 1)}, - }); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - // Construct route config to set retry policy. - RouteConfiguration new_route_config = default_route_config_; - auto* route1 = new_route_config.mutable_virtual_hosts(0)->mutable_routes(0); - auto* retry_policy = route1->mutable_route()->mutable_retry_policy(); - retry_policy->set_retry_on("deadline-exceeded"); - retry_policy->mutable_num_retries()->set_value(1); - // RetryBackoff is there but base interval is missing. - SetProtoDuration( - grpc_core::Duration::Milliseconds(250), - retry_policy->mutable_retry_back_off()->mutable_max_interval()); - SetRouteConfiguration(balancer_.get(), new_route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr( - "RouteAction RetryPolicy RetryBackoff missing base interval.")); -} - TEST_P(LdsRdsTest, XdsRoutingHeadersMatching) { CreateAndStartBackends(2); const char* kNewClusterName = "new_cluster"; @@ -2401,354 +2051,6 @@ TEST_P(LdsRdsTest, XdsRoutingChangeRoutesWithoutChangingClusters) { EXPECT_EQ(1, backends_[1]->backend_service2()->request_count()); } -// Test that we NACK unknown filter types in VirtualHost. -TEST_P(LdsRdsTest, RejectsUnknownHttpFilterTypeInVirtualHost) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = - route_config.mutable_virtual_hosts(0)->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom(Listener()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("no filter registered for config type " - "envoy.config.listener.v3.Listener")); -} - -// Test that we ignore optional unknown filter types in VirtualHost. -TEST_P(LdsRdsTest, IgnoresOptionalUnknownHttpFilterTypeInVirtualHost) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = - route_config.mutable_virtual_hosts(0)->mutable_typed_per_filter_config(); - ::envoy::config::route::v3::FilterConfig filter_config; - filter_config.mutable_config()->PackFrom(Listener()); - filter_config.set_is_optional(true); - (*per_filter_config)["unknown"].PackFrom(filter_config); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - WaitForAllBackends(DEBUG_LOCATION); - auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Test that we NACK filters without configs in VirtualHost. -TEST_P(LdsRdsTest, RejectsHttpFilterWithoutConfigInVirtualHost) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = - route_config.mutable_virtual_hosts(0)->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"]; - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "no filter config specified for filter name unknown")); -} - -// Test that we NACK filters without configs in FilterConfig in VirtualHost. -TEST_P(LdsRdsTest, RejectsHttpFilterWithoutConfigInFilterConfigInVirtualHost) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = - route_config.mutable_virtual_hosts(0)->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom( - ::envoy::config::route::v3::FilterConfig()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "no filter config specified for filter name unknown")); -} - -// Test that we ignore optional filters without configs in VirtualHost. -TEST_P(LdsRdsTest, IgnoresOptionalHttpFilterWithoutConfigInVirtualHost) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = - route_config.mutable_virtual_hosts(0)->mutable_typed_per_filter_config(); - ::envoy::config::route::v3::FilterConfig filter_config; - filter_config.set_is_optional(true); - (*per_filter_config)["unknown"].PackFrom(filter_config); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({ - {"locality0", CreateEndpointsForBackends()}, - }); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - WaitForAllBackends(DEBUG_LOCATION); - auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Test that we NACK unparseable filter types in VirtualHost. -TEST_P(LdsRdsTest, RejectsUnparseableHttpFilterTypeInVirtualHost) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = - route_config.mutable_virtual_hosts(0)->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom( - envoy::extensions::filters::http::router::v3::Router()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr("router filter does not support config override")); -} - -// Test that we NACK unknown filter types in Route. -TEST_P(LdsRdsTest, RejectsUnknownHttpFilterTypeInRoute) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom(Listener()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("no filter registered for config type " - "envoy.config.listener.v3.Listener")); -} - -// Test that we ignore optional unknown filter types in Route. -TEST_P(LdsRdsTest, IgnoresOptionalUnknownHttpFilterTypeInRoute) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_typed_per_filter_config(); - ::envoy::config::route::v3::FilterConfig filter_config; - filter_config.mutable_config()->PackFrom(Listener()); - filter_config.set_is_optional(true); - (*per_filter_config)["unknown"].PackFrom(filter_config); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - WaitForAllBackends(DEBUG_LOCATION); - auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Test that we NACK filters without configs in Route. -TEST_P(LdsRdsTest, RejectsHttpFilterWithoutConfigInRoute) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"]; - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "no filter config specified for filter name unknown")); -} - -// Test that we NACK filters without configs in FilterConfig in Route. -TEST_P(LdsRdsTest, RejectsHttpFilterWithoutConfigInFilterConfigInRoute) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom( - ::envoy::config::route::v3::FilterConfig()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "no filter config specified for filter name unknown")); -} - -// Test that we ignore optional filters without configs in Route. -TEST_P(LdsRdsTest, IgnoresOptionalHttpFilterWithoutConfigInRoute) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_typed_per_filter_config(); - ::envoy::config::route::v3::FilterConfig filter_config; - filter_config.set_is_optional(true); - (*per_filter_config)["unknown"].PackFrom(filter_config); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - WaitForAllBackends(DEBUG_LOCATION); - auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Test that we NACK unparseable filter types in Route. -TEST_P(LdsRdsTest, RejectsUnparseableHttpFilterTypeInRoute) { - RouteConfiguration route_config = default_route_config_; - auto* per_filter_config = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom( - envoy::extensions::filters::http::router::v3::Router()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr("router filter does not support config override")); -} - -// Test that we NACK unknown filter types in ClusterWeight. -TEST_P(LdsRdsTest, RejectsUnknownHttpFilterTypeInClusterWeight) { - RouteConfiguration route_config = default_route_config_; - auto* cluster_weight = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_route() - ->mutable_weighted_clusters() - ->add_clusters(); - cluster_weight->set_name(kDefaultClusterName); - cluster_weight->mutable_weight()->set_value(100); - auto* per_filter_config = cluster_weight->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom(Listener()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr("no filter registered for config type " - "envoy.config.listener.v3.Listener")); -} - -// Test that we ignore optional unknown filter types in ClusterWeight. -TEST_P(LdsRdsTest, IgnoresOptionalUnknownHttpFilterTypeInClusterWeight) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - auto* cluster_weight = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_route() - ->mutable_weighted_clusters() - ->add_clusters(); - cluster_weight->set_name(kDefaultClusterName); - cluster_weight->mutable_weight()->set_value(100); - auto* per_filter_config = cluster_weight->mutable_typed_per_filter_config(); - ::envoy::config::route::v3::FilterConfig filter_config; - filter_config.mutable_config()->PackFrom(Listener()); - filter_config.set_is_optional(true); - (*per_filter_config)["unknown"].PackFrom(filter_config); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - WaitForAllBackends(DEBUG_LOCATION); - auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Test that we NACK filters without configs in ClusterWeight. -TEST_P(LdsRdsTest, RejectsHttpFilterWithoutConfigInClusterWeight) { - RouteConfiguration route_config = default_route_config_; - auto* cluster_weight = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_route() - ->mutable_weighted_clusters() - ->add_clusters(); - cluster_weight->set_name(kDefaultClusterName); - cluster_weight->mutable_weight()->set_value(100); - auto* per_filter_config = cluster_weight->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"]; - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "no filter config specified for filter name unknown")); -} - -// Test that we NACK filters without configs in FilterConfig in ClusterWeight. -TEST_P(LdsRdsTest, - RejectsHttpFilterWithoutConfigInFilterConfigInClusterWeight) { - RouteConfiguration route_config = default_route_config_; - auto* cluster_weight = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_route() - ->mutable_weighted_clusters() - ->add_clusters(); - cluster_weight->set_name(kDefaultClusterName); - cluster_weight->mutable_weight()->set_value(100); - auto* per_filter_config = cluster_weight->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom( - ::envoy::config::route::v3::FilterConfig()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT(response_state->error_message, - ::testing::HasSubstr( - "no filter config specified for filter name unknown")); -} - -// Test that we ignore optional filters without configs in ClusterWeight. -TEST_P(LdsRdsTest, IgnoresOptionalHttpFilterWithoutConfigInClusterWeight) { - CreateAndStartBackends(1); - RouteConfiguration route_config = default_route_config_; - auto* cluster_weight = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_route() - ->mutable_weighted_clusters() - ->add_clusters(); - cluster_weight->set_name(kDefaultClusterName); - cluster_weight->mutable_weight()->set_value(100); - auto* per_filter_config = cluster_weight->mutable_typed_per_filter_config(); - ::envoy::config::route::v3::FilterConfig filter_config; - filter_config.set_is_optional(true); - (*per_filter_config)["unknown"].PackFrom(filter_config); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); - balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); - WaitForAllBackends(DEBUG_LOCATION); - auto response_state = RouteConfigurationResponseState(balancer_.get()); - ASSERT_TRUE(response_state.has_value()); - EXPECT_EQ(response_state->state, AdsServiceImpl::ResponseState::ACKED); -} - -// Test that we NACK unparseable filter types in ClusterWeight. -TEST_P(LdsRdsTest, RejectsUnparseableHttpFilterTypeInClusterWeight) { - RouteConfiguration route_config = default_route_config_; - auto* cluster_weight = route_config.mutable_virtual_hosts(0) - ->mutable_routes(0) - ->mutable_route() - ->mutable_weighted_clusters() - ->add_clusters(); - cluster_weight->set_name(kDefaultClusterName); - cluster_weight->mutable_weight()->set_value(100); - auto* per_filter_config = cluster_weight->mutable_typed_per_filter_config(); - (*per_filter_config)["unknown"].PackFrom( - envoy::extensions::filters::http::router::v3::Router()); - SetListenerAndRouteConfiguration(balancer_.get(), default_listener_, - route_config); - const auto response_state = WaitForRdsNack(DEBUG_LOCATION); - ASSERT_TRUE(response_state.has_value()) << "timed out waiting for NACK"; - EXPECT_THAT( - response_state->error_message, - ::testing::HasSubstr("router filter does not support config override")); -} - } // namespace } // namespace testing } // namespace grpc diff --git a/tools/run_tests/generated/tests.json b/tools/run_tests/generated/tests.json index 08d750fbc5f..ea50aab41eb 100644 --- a/tools/run_tests/generated/tests.json +++ b/tools/run_tests/generated/tests.json @@ -8529,6 +8529,30 @@ ], "uses_polling": true }, + { + "args": [], + "benchmark": false, + "ci_platforms": [ + "linux", + "mac", + "posix", + "windows" + ], + "cpu_cost": 1.0, + "exclude_configs": [], + "exclude_iomgrs": [], + "flaky": false, + "gtest": true, + "language": "c++", + "name": "xds_route_config_resource_type_test", + "platforms": [ + "linux", + "mac", + "posix", + "windows" + ], + "uses_polling": false + }, { "args": [], "boringssl": true,