diff --git a/source/common/config/subscription_factory_impl.cc b/source/common/config/subscription_factory_impl.cc index fee65c2a1d4e..d8448f30a6d0 100644 --- a/source/common/config/subscription_factory_impl.cc +++ b/source/common/config/subscription_factory_impl.cc @@ -59,7 +59,8 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( return std::make_unique( local_info_, cm_, api_config_source.cluster_names()[0], dispatcher_, random_, Utility::apiConfigSourceRefreshDelay(api_config_source), - Utility::apiConfigSourceRequestTimeout(api_config_source), restMethod(type_url), type_url, + Utility::apiConfigSourceRequestTimeout(api_config_source), + restMethod(type_url, api_config_source.transport_api_version()), type_url, api_config_source.transport_api_version(), callbacks, resource_decoder, stats, Utility::configSourceInitialFetchTimeout(config), validation_visitor_); case envoy::config::core::v3::ApiConfigSource::GRPC: @@ -69,8 +70,9 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( Utility::factoryForGrpcApiConfigSource(cm_.grpcAsyncClientManager(), api_config_source, scope, true) ->create(), - dispatcher_, sotwGrpcMethod(type_url), api_config_source.transport_api_version(), - random_, scope, Utility::parseRateLimitSettings(api_config_source), + dispatcher_, sotwGrpcMethod(type_url, api_config_source.transport_api_version()), + api_config_source.transport_api_version(), random_, scope, + Utility::parseRateLimitSettings(api_config_source), api_config_source.set_node_on_first_message_only()), callbacks, resource_decoder, stats, type_url, dispatcher_, Utility::configSourceInitialFetchTimeout(config), @@ -81,8 +83,9 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( Config::Utility::factoryForGrpcApiConfigSource(cm_.grpcAsyncClientManager(), api_config_source, scope, true) ->create(), - dispatcher_, deltaGrpcMethod(type_url), api_config_source.transport_api_version(), - random_, scope, Utility::parseRateLimitSettings(api_config_source), local_info_), + dispatcher_, deltaGrpcMethod(type_url, api_config_source.transport_api_version()), + api_config_source.transport_api_version(), random_, scope, + Utility::parseRateLimitSettings(api_config_source), local_info_), callbacks, resource_decoder, stats, type_url, dispatcher_, Utility::configSourceInitialFetchTimeout(config), false); } diff --git a/source/common/config/type_to_endpoint.cc b/source/common/config/type_to_endpoint.cc index 7fd3dd55ec2c..d7434aaa01f8 100644 --- a/source/common/config/type_to_endpoint.cc +++ b/source/common/config/type_to_endpoint.cc @@ -6,96 +6,252 @@ // API_NO_BOOST_FILE +#define SERVICE_VERSION_INFO(v2, v3) \ + createServiceVersionInfoMap(v2, {v2, v3}), createServiceVersionInfoMap(v3, {v2, v3}) + namespace Envoy { namespace Config { namespace { -// service RPC method fully qualified names. -struct Service { - std::string sotw_grpc_method_; - std::string delta_grpc_method_; - std::string rest_method_; +// A service's name, e.g. "envoy.api.v2.RouteDiscoveryService", +// "envoy.service.route.v3.RouteDiscoveryService". +using ServiceName = std::string; + +struct ServiceVersionInfo { + // This hold a name for each transport_api_version, for example for + // "envoy.api.v2.RouteDiscoveryService": + // { + // "V2": "envoy.api.v2.RouteDiscoveryService", + // "V3": "envoy.service.route.v3.RouteDiscoveryService" + // } + absl::flat_hash_map names_; +}; + +// A ServiceVersionInfoMap holds a service's transport_api_version and possible names for each +// available transport_api_version. For examples: +// +// Given "envoy.api.v2.RouteDiscoveryService" as the service name: +// { +// "envoy.api.v2.RouteDiscoveryService": { +// "names_": { +// "V2": "envoy.api.v2.RouteDiscoveryService", +// "V3": "envoy.service.route.v3.RouteDiscoveryService" +// } +// } +// } +// +// And for "envoy.service.route.v3.RouteDiscoveryService": +// { +// "envoy.service.route.v3.RouteDiscoveryService": +// "names_": { +// "V2": "envoy.api.v2.RouteDiscoveryService", +// "V3": "envoy.service.route.v3.RouteDiscoveryService" +// } +// } +// } +using ServiceVersionInfoMap = absl::flat_hash_map; + +// This creates a ServiceVersionInfoMap, with service name (For example: +// "envoy.api.v2.RouteDiscoveryService") as the key. +ServiceVersionInfoMap +createServiceVersionInfoMap(absl::string_view service_name, + const std::array& versioned_service_names) { + const auto key = static_cast(service_name); + return ServiceVersionInfoMap{{ + // ServiceName as the key. + key, + + // ServiceVersionInfo as the value. + ServiceVersionInfo{{ + {envoy::config::core::v3::ApiVersion::V2, versioned_service_names[0]}, + {envoy::config::core::v3::ApiVersion::V3, versioned_service_names[1]}, + }}, + }}; +} + +// A resource type URL. For example: "type.googleapis.com/envoy.api.v2.RouteConfiguration". +using TypeUrl = std::string; + +TypeUrl getResourceTypeUrl(absl::string_view service_name) { + const auto* service_desc = Protobuf::DescriptorPool::generated_pool()->FindServiceByName( + static_cast(service_name)); + ASSERT(service_desc != nullptr, fmt::format("{} missing", service_name)); + ASSERT(service_desc->options().HasExtension(envoy::annotations::resource)); + + return Grpc::Common::typeUrl( + service_desc->options().GetExtension(envoy::annotations::resource).type()); +} + +// A method name, e.g. "envoy.api.v2.RouteDiscoveryService.StreamRoutes". +using MethodName = std::string; + +struct VersionedDiscoveryType { + // A map of transport_api_version to discovery service RPC method fully qualified names. e.g. + // { + // "V2": "envoy.api.v2.RouteDiscoveryService.StreamRoutes", + // "V3": "envoy.service.route.v3.RouteDiscoveryService.StreamRoutes" + // } + absl::flat_hash_map methods_; +}; + +// This holds versioned discovery types. +struct VersionedService { + VersionedDiscoveryType sotw_grpc_; + VersionedDiscoveryType delta_grpc_; + VersionedDiscoveryType rest_; }; -// Map from resource type URL to service RPC methods. -using TypeUrlToServiceMap = std::unordered_map; +using TypeUrlToVersionedServiceMap = absl::flat_hash_map; + +// buildTypeUrlToServiceMap() builds a reverse map from a resource type URLs to a versioned service +// (by transport_api_version). +// +// The way we build it is by firstly constructing a list of ServiceVersionInfoMap: +// [ +// { +// "envoy.api.v2.RouteDiscoveryService": { +// "names_": { +// "V2": "envoy.api.v2.RouteDiscoveryService", +// "V3": "envoy.service.route.v3.RouteDiscoveryService" +// } +// } +// }, +// { +// "envoy.service.route.v3.RouteDiscoveryService": { +// "names_": { +// "V2": "envoy.api.v2.RouteDiscoveryService", +// "V3": "envoy.service.route.v3.RouteDiscoveryService" +// } +// } +// } +// ... +// ] +// +// Then we convert it into the following map, with the inferred resource type URL as the key: +// +// { +// "type.googleapis.com/envoy.api.v2.RouteConfiguration": { +// "sotw_grpc_": { +// "methods_": { +// "V2": "envoy.api.v2.RouteDiscoveryService.StreamRoutes", +// "V3": "envoy.service.route.v3.RouteDiscoveryService.StreamRoutes" +// } +// }, +// ... +// }, +// "type.googleapis.com/envoy.config.route.v3.RouteConfiguration": { +// "sotw_grpc_": { +// "methods_": { +// "V2": "envoy.api.v2.RouteDiscoveryService.StreamRoutes", +// "V3": "envoy.service.route.v3.RouteDiscoveryService.StreamRoutes" +// } +// }, +// ... +// } +// } +// +TypeUrlToVersionedServiceMap* buildTypeUrlToServiceMap() { + auto* type_url_to_versioned_service_map = new TypeUrlToVersionedServiceMap(); -TypeUrlToServiceMap* buildTypeUrlToServiceMap() { - auto* type_url_to_service_map = new TypeUrlToServiceMap(); // This happens once in the lifetime of Envoy. We build a reverse map from resource type URL to - // service methods. We explicitly enumerate all services, since DescriptorPool doesn't support - // iterating over all descriptors, due its lazy load design, see - // https://www.mail-archive.com/protobuf@googlegroups.com/msg04540.html. - for (const std::string& service_name : { - "envoy.api.v2.RouteDiscoveryService", - "envoy.service.route.v3.RouteDiscoveryService", - "envoy.api.v2.ScopedRoutesDiscoveryService", - "envoy.service.route.v3.ScopedRoutesDiscoveryService", - "envoy.api.v2.VirtualHostDiscoveryService", - "envoy.service.route.v3.VirtualHostDiscoveryService", - "envoy.service.discovery.v2.SecretDiscoveryService", - "envoy.service.secret.v3.SecretDiscoveryService", - "envoy.api.v2.ClusterDiscoveryService", - "envoy.service.cluster.v3.ClusterDiscoveryService", - "envoy.api.v2.EndpointDiscoveryService", - "envoy.service.endpoint.v3.EndpointDiscoveryService", - "envoy.api.v2.ListenerDiscoveryService", - "envoy.service.listener.v3.ListenerDiscoveryService", - "envoy.service.discovery.v2.RuntimeDiscoveryService", - "envoy.service.runtime.v3.RuntimeDiscoveryService", + // service methods (versioned by transport_api_version). We explicitly enumerate all services, + // since DescriptorPool doesn't support iterating over all descriptors, due its lazy load design, + // see https://www.mail-archive.com/protobuf@googlegroups.com/msg04540.html. + for (const ServiceVersionInfoMap& registered : { + SERVICE_VERSION_INFO("envoy.api.v2.RouteDiscoveryService", + "envoy.service.route.v3.RouteDiscoveryService"), + SERVICE_VERSION_INFO("envoy.api.v2.ScopedRoutesDiscoveryService", + "envoy.service.route.v3.ScopedRoutesDiscoveryService"), + SERVICE_VERSION_INFO("envoy.api.v2.ScopedRoutesDiscoveryService", + "envoy.service.route.v3.ScopedRoutesDiscoveryService"), + SERVICE_VERSION_INFO("envoy.api.v2.VirtualHostDiscoveryService", + "envoy.service.route.v3.VirtualHostDiscoveryService"), + SERVICE_VERSION_INFO("envoy.service.discovery.v2.SecretDiscoveryService", + "envoy.service.secret.v3.SecretDiscoveryService"), + SERVICE_VERSION_INFO("envoy.api.v2.ClusterDiscoveryService", + "envoy.service.cluster.v3.ClusterDiscoveryService"), + SERVICE_VERSION_INFO("envoy.api.v2.EndpointDiscoveryService", + "envoy.service.endpoint.v3.EndpointDiscoveryService"), + SERVICE_VERSION_INFO("envoy.api.v2.ListenerDiscoveryService", + "envoy.service.listener.v3.ListenerDiscoveryService"), + SERVICE_VERSION_INFO("envoy.service.discovery.v2.RuntimeDiscoveryService", + "envoy.service.runtime.v3.RuntimeDiscoveryService"), }) { - const auto* service_desc = - Protobuf::DescriptorPool::generated_pool()->FindServiceByName(service_name); - // TODO(htuch): this should become an ASSERT once all v3 descriptors are linked in. - ASSERT(service_desc != nullptr, fmt::format("{} missing", service_name)); - ASSERT(service_desc->options().HasExtension(envoy::annotations::resource)); - const std::string resource_type_url = Grpc::Common::typeUrl( - service_desc->options().GetExtension(envoy::annotations::resource).type()); - Service& service = (*type_url_to_service_map)[resource_type_url]; - // We populate the service methods that are known below, but it's possible that some services - // don't implement all, e.g. VHDS doesn't support SotW or REST. - for (int method_index = 0; method_index < service_desc->method_count(); ++method_index) { - const auto& method_desc = *service_desc->method(method_index); - if (absl::StartsWith(method_desc.name(), "Stream")) { - service.sotw_grpc_method_ = method_desc.full_name(); - } else if (absl::StartsWith(method_desc.name(), "Delta")) { - service.delta_grpc_method_ = method_desc.full_name(); - } else if (absl::StartsWith(method_desc.name(), "Fetch")) { - service.rest_method_ = method_desc.full_name(); - } else { - ASSERT(false, "Unknown xDS service method"); + for (const auto& registered_service : registered) { + const TypeUrl resource_type_url = getResourceTypeUrl(registered_service.first); + VersionedService& service = (*type_url_to_versioned_service_map)[resource_type_url]; + + for (const auto& versioned_service_name : registered_service.second.names_) { + const ServiceName& service_name = versioned_service_name.second; + const auto* service_desc = + Protobuf::DescriptorPool::generated_pool()->FindServiceByName(service_name); + ASSERT(service_desc != nullptr, fmt::format("{} missing", service_name)); + ASSERT(service_desc->options().HasExtension(envoy::annotations::resource)); + + // We populate the service methods that are known below, but it's possible that some + // services don't implement all, e.g. VHDS doesn't support SotW or REST. + for (int method_index = 0; method_index < service_desc->method_count(); ++method_index) { + const auto& method_desc = *service_desc->method(method_index); + const auto transport_api_version = versioned_service_name.first; + if (absl::StartsWith(method_desc.name(), "Stream")) { + service.sotw_grpc_.methods_[transport_api_version] = method_desc.full_name(); + } else if (absl::StartsWith(method_desc.name(), "Delta")) { + service.delta_grpc_.methods_[transport_api_version] = method_desc.full_name(); + } else if (absl::StartsWith(method_desc.name(), "Fetch")) { + service.rest_.methods_[transport_api_version] = method_desc.full_name(); + } else { + ASSERT(false, "Unknown xDS service method"); + } + } } } } - return type_url_to_service_map; + return type_url_to_versioned_service_map; } -TypeUrlToServiceMap& typeUrlToServiceMap() { - static TypeUrlToServiceMap* type_url_to_service_map = buildTypeUrlToServiceMap(); - return *type_url_to_service_map; +TypeUrlToVersionedServiceMap& typeUrlToVersionedServiceMap() { + static TypeUrlToVersionedServiceMap* type_url_to_versioned_service_map = + buildTypeUrlToServiceMap(); + return *type_url_to_versioned_service_map; +} + +envoy::config::core::v3::ApiVersion +effectiveTransportApiVersion(envoy::config::core::v3::ApiVersion transport_api_version) { + // By default (when the transport_api_version is "AUTO"), the effective transport_api_version is + // envoy::config::core::v3::ApiVersion::V2. + if (transport_api_version == envoy::config::core::v3::ApiVersion::AUTO) { + return envoy::config::core::v3::ApiVersion::V2; + } + return transport_api_version; } } // namespace -const Protobuf::MethodDescriptor& deltaGrpcMethod(absl::string_view type_url) { - const auto it = typeUrlToServiceMap().find(static_cast(type_url)); - ASSERT(it != typeUrlToServiceMap().cend()); +const Protobuf::MethodDescriptor& +deltaGrpcMethod(absl::string_view type_url, + envoy::config::core::v3::ApiVersion transport_api_version) { + const auto it = typeUrlToVersionedServiceMap().find(static_cast(type_url)); + ASSERT(it != typeUrlToVersionedServiceMap().cend()); return *Protobuf::DescriptorPool::generated_pool()->FindMethodByName( - it->second.delta_grpc_method_); + it->second.delta_grpc_.methods_[effectiveTransportApiVersion(transport_api_version)]); } -const Protobuf::MethodDescriptor& sotwGrpcMethod(absl::string_view type_url) { - const auto it = typeUrlToServiceMap().find(static_cast(type_url)); - ASSERT(it != typeUrlToServiceMap().cend()); +const Protobuf::MethodDescriptor& +sotwGrpcMethod(absl::string_view type_url, + envoy::config::core::v3::ApiVersion transport_api_version) { + const auto it = typeUrlToVersionedServiceMap().find(static_cast(type_url)); + ASSERT(it != typeUrlToVersionedServiceMap().cend()); return *Protobuf::DescriptorPool::generated_pool()->FindMethodByName( - it->second.sotw_grpc_method_); + it->second.sotw_grpc_.methods_[effectiveTransportApiVersion(transport_api_version)]); } -const Protobuf::MethodDescriptor& restMethod(absl::string_view type_url) { - const auto it = typeUrlToServiceMap().find(static_cast(type_url)); - ASSERT(it != typeUrlToServiceMap().cend()); - return *Protobuf::DescriptorPool::generated_pool()->FindMethodByName(it->second.rest_method_); +const Protobuf::MethodDescriptor& +restMethod(absl::string_view type_url, envoy::config::core::v3::ApiVersion transport_api_version) { + const auto it = typeUrlToVersionedServiceMap().find(static_cast(type_url)); + ASSERT(it != typeUrlToVersionedServiceMap().cend()); + return *Protobuf::DescriptorPool::generated_pool()->FindMethodByName( + it->second.rest_.methods_[effectiveTransportApiVersion(transport_api_version)]); } } // namespace Config diff --git a/source/common/config/type_to_endpoint.h b/source/common/config/type_to_endpoint.h index 0f06d2007977..ed9f9e6e5c89 100644 --- a/source/common/config/type_to_endpoint.h +++ b/source/common/config/type_to_endpoint.h @@ -10,12 +10,18 @@ namespace Envoy { namespace Config { // Translates an xDS resource type_url to the name of the delta gRPC service that carries it. -const Protobuf::MethodDescriptor& deltaGrpcMethod(absl::string_view resource_type_url); +const Protobuf::MethodDescriptor& +deltaGrpcMethod(absl::string_view resource_type_url, + envoy::config::core::v3::ApiVersion transport_api_version); // Translates an xDS resource type_url to the name of the state-of-the-world gRPC service that // carries it. -const Protobuf::MethodDescriptor& sotwGrpcMethod(absl::string_view resource_type_url); +const Protobuf::MethodDescriptor& +sotwGrpcMethod(absl::string_view resource_type_url, + envoy::config::core::v3::ApiVersion transport_api_version); // Translates an xDS resource type_url to the name of the REST service that carries it. -const Protobuf::MethodDescriptor& restMethod(absl::string_view resource_type_url); +const Protobuf::MethodDescriptor& +restMethod(absl::string_view resource_type_url, + envoy::config::core::v3::ApiVersion transport_api_version); } // namespace Config } // namespace Envoy diff --git a/test/common/config/type_to_endpoint_test.cc b/test/common/config/type_to_endpoint_test.cc index 353580f17244..f163d832218c 100644 --- a/test/common/config/type_to_endpoint_test.cc +++ b/test/common/config/type_to_endpoint_test.cc @@ -16,23 +16,87 @@ TEST(TypeToEndpoint, All) { // The dummy messages are included for link purposes only. envoy::api::v2::RdsDummy _v2_rds_dummy; envoy::service::route::v3::RdsDummy _v3_rds_dummy; + // Delta gRPC endpoints. EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.DeltaRoutes", - deltaGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration").full_name()); - EXPECT_EQ( - "envoy.service.route.v3.RouteDiscoveryService.DeltaRoutes", - deltaGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration").full_name()); + deltaGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::AUTO) + .full_name()); + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.DeltaRoutes", + deltaGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V2) + .full_name()); + EXPECT_EQ("envoy.service.route.v3.RouteDiscoveryService.DeltaRoutes", + deltaGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V3) + .full_name()); + + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.DeltaRoutes", + deltaGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::AUTO) + .full_name()); + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.DeltaRoutes", + deltaGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V2) + .full_name()); + EXPECT_EQ("envoy.service.route.v3.RouteDiscoveryService.DeltaRoutes", + deltaGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V3) + .full_name()); + // SotW gRPC endpoints. EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.StreamRoutes", - sotwGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration").full_name()); - EXPECT_EQ( - "envoy.service.route.v3.RouteDiscoveryService.StreamRoutes", - sotwGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration").full_name()); + sotwGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::AUTO) + .full_name()); + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.StreamRoutes", + sotwGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V2) + .full_name()); + EXPECT_EQ("envoy.service.route.v3.RouteDiscoveryService.StreamRoutes", + sotwGrpcMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V3) + .full_name()); + + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.StreamRoutes", + sotwGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::AUTO) + .full_name()); + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.StreamRoutes", + sotwGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V2) + .full_name()); + EXPECT_EQ("envoy.service.route.v3.RouteDiscoveryService.StreamRoutes", + sotwGrpcMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V3) + .full_name()); + // REST endpoints. EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.FetchRoutes", - restMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration").full_name()); + restMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::AUTO) + .full_name()); + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.FetchRoutes", + restMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V2) + .full_name()); + EXPECT_EQ("envoy.service.route.v3.RouteDiscoveryService.FetchRoutes", + restMethod("type.googleapis.com/envoy.api.v2.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V3) + .full_name()); + + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.FetchRoutes", + restMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::AUTO) + .full_name()); + EXPECT_EQ("envoy.api.v2.RouteDiscoveryService.FetchRoutes", + restMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V2) + .full_name()); EXPECT_EQ("envoy.service.route.v3.RouteDiscoveryService.FetchRoutes", - restMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration").full_name()); + restMethod("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + envoy::config::core::v3::ApiVersion::V3) + .full_name()); } } // namespace