diff --git a/source/server/admin/BUILD b/source/server/admin/BUILD index b91ff656d1684..0124f59e457f7 100644 --- a/source/server/admin/BUILD +++ b/source/server/admin/BUILD @@ -15,6 +15,7 @@ envoy_cc_library( deps = [ ":admin_filter_lib", ":clusters_handler_lib", + ":config_dump_handler_lib", ":config_tracker_lib", ":listeners_handler_lib", ":logs_handler_lib", @@ -62,11 +63,6 @@ envoy_cc_library( "//source/common/router:scoped_config_lib", "//source/common/stats:isolated_store_lib", "//source/extensions/access_loggers/file:file_access_log_lib", - "@envoy_api//envoy/admin/v3:pkg_cc_proto", - "@envoy_api//envoy/config/core/v3:pkg_cc_proto", - "@envoy_api//envoy/config/endpoint/v3:pkg_cc_proto", - "@envoy_api//envoy/config/route/v3:pkg_cc_proto", - "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", ], ) @@ -243,6 +239,26 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "config_dump_handler_lib", + srcs = ["config_dump_handler.cc"], + hdrs = ["config_dump_handler.h"], + deps = [ + ":config_tracker_lib", + ":handler_ctx_lib", + ":utils_lib", + "//include/envoy/http:codes_interface", + "//include/envoy/server:admin_interface", + "//include/envoy/server:instance_interface", + "//source/common/buffer:buffer_lib", + "//source/common/http:codes_lib", + "//source/common/http:header_map_lib", + "@envoy_api//envoy/admin/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/endpoint/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "utils_lib", srcs = ["utils.cc"], diff --git a/source/server/admin/admin.cc b/source/server/admin/admin.cc index cb62f9b0e9fb6..2f7d1e58b108d 100644 --- a/source/server/admin/admin.cc +++ b/source/server/admin/admin.cc @@ -7,12 +7,6 @@ #include #include -#include "envoy/admin/v3/certs.pb.h" -#include "envoy/admin/v3/config_dump.pb.h" -#include "envoy/admin/v3/metrics.pb.h" -#include "envoy/admin/v3/server_info.pb.h" -#include "envoy/config/core/v3/health_check.pb.h" -#include "envoy/config/endpoint/v3/endpoint_components.pb.h" #include "envoy/filesystem/filesystem.h" #include "envoy/server/hot_restart.h" #include "envoy/server/instance.h" @@ -118,297 +112,8 @@ const char AdminHtmlEnd[] = R"( )"; -// Helper method to get the resource parameter. -absl::optional resourceParam(const Http::Utility::QueryParams& params) { - return Utility::queryParam(params, "resource"); -} - -// Helper method to get the mask parameter. -absl::optional maskParam(const Http::Utility::QueryParams& params) { - return Utility::queryParam(params, "mask"); -} - -// Helper method to get the eds parameter. -bool shouldIncludeEdsInDump(const Http::Utility::QueryParams& params) { - return Utility::queryParam(params, "include_eds") != absl::nullopt; -} - -// Apply a field mask to a resource message. A simple field mask might look -// like "cluster.name,cluster.alt_stat_name,last_updated" for a StaticCluster -// resource. Unfortunately, since the "cluster" field is Any and the in-built -// FieldMask utils can't mask inside an Any field, we need to do additional work -// below. -// -// We take advantage of the fact that for the most part (with the exception of -// DynamicListener) that ConfigDump resources have a single Any field where the -// embedded resources lives. This allows us to construct an inner field mask for -// the Any resource and an outer field mask for the enclosing message. In the -// above example, the inner field mask would be "name,alt_stat_name" and the -// outer field mask "cluster,last_updated". The masks are applied to their -// respective messages, with the Any resource requiring an unpack/mask/pack -// series of operations. -// -// TODO(htuch): we could make field masks more powerful in future and generalize -// this to allow arbitrary indexing through Any fields. This is pretty -// complicated, we would need to build a FieldMask tree similar to how the C++ -// Protobuf library does this internally. -void trimResourceMessage(const Protobuf::FieldMask& field_mask, Protobuf::Message& message) { - const Protobuf::Descriptor* descriptor = message.GetDescriptor(); - const Protobuf::Reflection* reflection = message.GetReflection(); - // Figure out which paths cover Any fields. For each field, gather the paths to - // an inner mask, switch the outer mask to cover only the original field. - Protobuf::FieldMask outer_field_mask; - Protobuf::FieldMask inner_field_mask; - std::string any_field_name; - for (int i = 0; i < field_mask.paths().size(); ++i) { - const std::string& path = field_mask.paths(i); - std::vector frags = absl::StrSplit(path, '.'); - if (frags.empty()) { - continue; - } - const Protobuf::FieldDescriptor* field = descriptor->FindFieldByName(frags[0]); - // Only a single Any field supported, repeated fields don't support further - // indexing. - // TODO(htuch): should add support for DynamicListener for multiple Any - // fields in the future, see - // https://github.com/envoyproxy/envoy/issues/9669. - if (field != nullptr && field->message_type() != nullptr && !field->is_repeated() && - field->message_type()->full_name() == "google.protobuf.Any") { - if (any_field_name.empty()) { - any_field_name = frags[0]; - } else { - // This should be structurally true due to the ConfigDump proto - // definition (but not for DynamicListener today). - ASSERT(any_field_name == frags[0], - "Only a single Any field in a config dump resource is supported."); - } - outer_field_mask.add_paths(frags[0]); - frags.erase(frags.begin()); - inner_field_mask.add_paths(absl::StrJoin(frags, ".")); - } else { - outer_field_mask.add_paths(path); - } - } - - if (!any_field_name.empty()) { - const Protobuf::FieldDescriptor* any_field = descriptor->FindFieldByName(any_field_name); - if (reflection->HasField(message, any_field)) { - ASSERT(any_field != nullptr); - // Unpack to a DynamicMessage. - ProtobufWkt::Any any_message; - any_message.MergeFrom(reflection->GetMessage(message, any_field)); - Protobuf::DynamicMessageFactory dmf; - const absl::string_view inner_type_name = - TypeUtil::typeUrlToDescriptorFullName(any_message.type_url()); - const Protobuf::Descriptor* inner_descriptor = - Protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName( - static_cast(inner_type_name)); - ASSERT(inner_descriptor != nullptr); - std::unique_ptr inner_message; - inner_message.reset(dmf.GetPrototype(inner_descriptor)->New()); - MessageUtil::unpackTo(any_message, *inner_message); - // Trim message. - ProtobufUtil::FieldMaskUtil::TrimMessage(inner_field_mask, inner_message.get()); - // Pack it back into the Any resource. - any_message.PackFrom(*inner_message); - reflection->MutableMessage(&message, any_field)->CopyFrom(any_message); - } - } - ProtobufUtil::FieldMaskUtil::TrimMessage(outer_field_mask, &message); -} - } // namespace -void AdminImpl::addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, - const absl::optional& mask, - bool include_eds) const { - Envoy::Server::ConfigTracker::CbsMap callbacks_map = config_tracker_.getCallbacksMap(); - if (include_eds) { - if (!server_.clusterManager().clusters().empty()) { - callbacks_map.emplace("endpoint", [this] { return dumpEndpointConfigs(); }); - } - } - - for (const auto& [name, callback] : callbacks_map) { - ProtobufTypes::MessagePtr message = callback(); - ASSERT(message); - - if (mask.has_value()) { - Protobuf::FieldMask field_mask; - ProtobufUtil::FieldMaskUtil::FromString(mask.value(), &field_mask); - // We don't use trimMessage() above here since masks don't support - // indexing through repeated fields. - ProtobufUtil::FieldMaskUtil::TrimMessage(field_mask, message.get()); - } - - auto* config = dump.add_configs(); - config->PackFrom(*message); - } -} - -absl::optional> -AdminImpl::addResourceToDump(envoy::admin::v3::ConfigDump& dump, - const absl::optional& mask, const std::string& resource, - bool include_eds) const { - Envoy::Server::ConfigTracker::CbsMap callbacks_map = config_tracker_.getCallbacksMap(); - if (include_eds) { - if (!server_.clusterManager().clusters().empty()) { - callbacks_map.emplace("endpoint", [this] { return dumpEndpointConfigs(); }); - } - } - - for (const auto& [name, callback] : callbacks_map) { - ProtobufTypes::MessagePtr message = callback(); - ASSERT(message); - - auto field_descriptor = message->GetDescriptor()->FindFieldByName(resource); - const Protobuf::Reflection* reflection = message->GetReflection(); - if (!field_descriptor) { - continue; - } else if (!field_descriptor->is_repeated()) { - return absl::optional>{std::make_pair( - Http::Code::BadRequest, - fmt::format("{} is not a repeated field. Use ?mask={} to get only this field", - field_descriptor->name(), field_descriptor->name()))}; - } - - auto repeated = reflection->GetRepeatedPtrField(*message, field_descriptor); - for (Protobuf::Message& msg : repeated) { - if (mask.has_value()) { - Protobuf::FieldMask field_mask; - ProtobufUtil::FieldMaskUtil::FromString(mask.value(), &field_mask); - trimResourceMessage(field_mask, msg); - } - auto* config = dump.add_configs(); - config->PackFrom(msg); - } - - // We found the desired resource so there is no need to continue iterating over - // the other keys. - return absl::nullopt; - } - - return absl::optional>{ - std::make_pair(Http::Code::NotFound, fmt::format("{} not found in config dump", resource))}; -} - -void AdminImpl::addLbEndpoint( - const Upstream::HostSharedPtr& host, - envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint) const { - auto& lb_endpoint = *locality_lb_endpoint.mutable_lb_endpoints()->Add(); - if (host->metadata() != nullptr) { - lb_endpoint.mutable_metadata()->MergeFrom(*host->metadata()); - } - lb_endpoint.mutable_load_balancing_weight()->set_value(host->weight()); - - switch (host->health()) { - case Upstream::Host::Health::Healthy: - lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::HEALTHY); - break; - case Upstream::Host::Health::Unhealthy: - lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::UNHEALTHY); - break; - case Upstream::Host::Health::Degraded: - lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::DEGRADED); - break; - default: - lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::UNKNOWN); - } - - auto& endpoint = *lb_endpoint.mutable_endpoint(); - endpoint.set_hostname(host->hostname()); - Network::Utility::addressToProtobufAddress(*host->address(), *endpoint.mutable_address()); - auto& health_check_config = *endpoint.mutable_health_check_config(); - health_check_config.set_hostname(host->hostnameForHealthChecks()); - if (host->healthCheckAddress()->asString() != host->address()->asString()) { - health_check_config.set_port_value(host->healthCheckAddress()->ip()->port()); - } -} - -ProtobufTypes::MessagePtr AdminImpl::dumpEndpointConfigs() const { - auto endpoint_config_dump = std::make_unique(); - - for (const auto& [name, cluster_ref] : server_.clusterManager().clusters()) { - const Upstream::Cluster& cluster = cluster_ref.get(); - Upstream::ClusterInfoConstSharedPtr cluster_info = cluster.info(); - envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; - - if (cluster_info->edsServiceName().has_value()) { - cluster_load_assignment.set_cluster_name(cluster_info->edsServiceName().value()); - } else { - cluster_load_assignment.set_cluster_name(cluster_info->name()); - } - auto& policy = *cluster_load_assignment.mutable_policy(); - - for (auto& host_set : cluster.prioritySet().hostSetsPerPriority()) { - policy.mutable_overprovisioning_factor()->set_value(host_set->overprovisioningFactor()); - - if (!host_set->hostsPerLocality().get().empty()) { - for (int index = 0; index < static_cast(host_set->hostsPerLocality().get().size()); - index++) { - auto locality_host_set = host_set->hostsPerLocality().get()[index]; - - if (!locality_host_set.empty()) { - auto& locality_lb_endpoint = *cluster_load_assignment.mutable_endpoints()->Add(); - locality_lb_endpoint.mutable_locality()->MergeFrom(locality_host_set[0]->locality()); - locality_lb_endpoint.set_priority(locality_host_set[0]->priority()); - if (host_set->localityWeights() != nullptr && !host_set->localityWeights()->empty()) { - locality_lb_endpoint.mutable_load_balancing_weight()->set_value( - (*host_set->localityWeights())[index]); - } - - for (auto& host : locality_host_set) { - addLbEndpoint(host, locality_lb_endpoint); - } - } - } - } else { - for (auto& host : host_set->hosts()) { - auto& locality_lb_endpoint = *cluster_load_assignment.mutable_endpoints()->Add(); - locality_lb_endpoint.mutable_locality()->MergeFrom(host->locality()); - locality_lb_endpoint.set_priority(host->priority()); - addLbEndpoint(host, locality_lb_endpoint); - } - } - } - - if (cluster_info->addedViaApi()) { - auto& dynamic_endpoint = *endpoint_config_dump->mutable_dynamic_endpoint_configs()->Add(); - dynamic_endpoint.mutable_endpoint_config()->PackFrom(cluster_load_assignment); - } else { - auto& static_endpoint = *endpoint_config_dump->mutable_static_endpoint_configs()->Add(); - static_endpoint.mutable_endpoint_config()->PackFrom(cluster_load_assignment); - } - } - return endpoint_config_dump; -} - -Http::Code AdminImpl::handlerConfigDump(absl::string_view url, - Http::ResponseHeaderMap& response_headers, - Buffer::Instance& response, AdminStream&) const { - Http::Utility::QueryParams query_params = Http::Utility::parseAndDecodeQueryString(url); - const auto resource = resourceParam(query_params); - const auto mask = maskParam(query_params); - const bool include_eds = shouldIncludeEdsInDump(query_params); - - envoy::admin::v3::ConfigDump dump; - - if (resource.has_value()) { - auto err = addResourceToDump(dump, mask, resource.value(), include_eds); - if (err.has_value()) { - response.add(err.value().second); - return err.value().first; - } - } else { - addAllConfigToDump(dump, mask, include_eds); - } - MessageUtil::redact(dump); - - response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json); - response.add(MessageUtil::getJsonStringFromMessage(dump, true)); // pretty-print - return Http::Code::OK; -} - ConfigTracker& AdminImpl::getConfigTracker() { return config_tracker_; } AdminImpl::NullRouteConfigProvider::NullRouteConfigProvider(TimeSource& time_source) @@ -449,9 +154,9 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server) Http::ConnectionManagerImpl::generateTracingStats("http.admin.", no_op_store_)), route_config_provider_(server.timeSource()), scoped_route_config_provider_(server.timeSource()), clusters_handler_(server), - stats_handler_(server), logs_handler_(server), profiling_handler_(profile_path), - runtime_handler_(server), listeners_handler_(server), server_cmd_handler_(server), - server_info_handler_(server), + config_dump_handler_(config_tracker_, server), stats_handler_(server), logs_handler_(server), + profiling_handler_(profile_path), runtime_handler_(server), listeners_handler_(server), + server_cmd_handler_(server), server_info_handler_(server), // TODO(jsedgwick) add /runtime_reset endpoint that removes all admin-set values handlers_{ {"/", "Admin home page", MAKE_ADMIN_HANDLER(handlerAdminHome), false, false}, @@ -460,7 +165,7 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server) {"/clusters", "upstream cluster status", MAKE_ADMIN_HANDLER(clusters_handler_.handlerClusters), false, false}, {"/config_dump", "dump current Envoy configs (experimental)", - MAKE_ADMIN_HANDLER(handlerConfigDump), false, false}, + MAKE_ADMIN_HANDLER(config_dump_handler_.handlerConfigDump), false, false}, {"/contention", "dump current Envoy mutex contention stats (if enabled)", MAKE_ADMIN_HANDLER(stats_handler_.handlerContention), false, false}, {"/cpuprofiler", "enable/disable the CPU profiler", diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index 583ae5dd32664..c1f89cfcbe910 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -7,11 +7,6 @@ #include #include -#include "envoy/admin/v3/config_dump.pb.h" -#include "envoy/admin/v3/server_info.pb.h" -#include "envoy/config/core/v3/base.pb.h" -#include "envoy/config/route/v3/route.pb.h" -#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/http/filter.h" #include "envoy/http/request_id_extension.h" #include "envoy/network/filter.h" @@ -42,6 +37,7 @@ #include "server/admin/admin_filter.h" #include "server/admin/clusters_handler.h" +#include "server/admin/config_dump_handler.h" #include "server/admin/config_tracker_impl.h" #include "server/admin/listeners_handler.h" #include "server/admin/logs_handler.h" @@ -282,38 +278,15 @@ class AdminImpl : public Admin, ThreadLocal::SlotPtr tls_; }; - /** - * Helper methods for the /config_dump url handler. - */ - void addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, - const absl::optional& mask, bool include_eds) const; - /** - * Add the config matching the passed resource to the passed config dump. - * @return absl::nullopt on success, else the Http::Code and an error message that should be added - * to the admin response. - */ - absl::optional> - addResourceToDump(envoy::admin::v3::ConfigDump& dump, const absl::optional& mask, - const std::string& resource, bool include_eds) const; - std::vector sortedHandlers() const; envoy::admin::v3::ServerInfo::State serverState(); - /** - * Helper methods for the /config_dump url handler to add endpoints config - */ - void addLbEndpoint(const Upstream::HostSharedPtr& host, - envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint) const; - ProtobufTypes::MessagePtr dumpEndpointConfigs() const; /** * URL handlers. */ Http::Code handlerAdminHome(absl::string_view path_and_query, Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, AdminStream&); - Http::Code handlerConfigDump(absl::string_view path_and_query, - Http::ResponseHeaderMap& response_headers, - Buffer::Instance& response, AdminStream&) const; Http::Code handlerHelp(absl::string_view path_and_query, Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, AdminStream&); @@ -424,6 +397,7 @@ class AdminImpl : public Admin, NullRouteConfigProvider route_config_provider_; NullScopedRouteConfigProvider scoped_route_config_provider_; Server::ClustersHandler clusters_handler_; + Server::ConfigDumpHandler config_dump_handler_; Server::StatsHandler stats_handler_; Server::LogsHandler logs_handler_; Server::ProfilingHandler profiling_handler_; diff --git a/source/server/admin/config_dump_handler.cc b/source/server/admin/config_dump_handler.cc new file mode 100644 index 0000000000000..dbfd13a01e2ec --- /dev/null +++ b/source/server/admin/config_dump_handler.cc @@ -0,0 +1,311 @@ +#include "server/admin/config_dump_handler.h" + +#include "envoy/config/core/v3/health_check.pb.h" +#include "envoy/config/endpoint/v3/endpoint.pb.h" + +#include "common/http/headers.h" +#include "common/http/utility.h" +#include "common/network/utility.h" + +#include "server/admin/utils.h" + +namespace Envoy { +namespace Server { + +namespace { +// Apply a field mask to a resource message. A simple field mask might look +// like "cluster.name,cluster.alt_stat_name,last_updated" for a StaticCluster +// resource. Unfortunately, since the "cluster" field is Any and the in-built +// FieldMask utils can't mask inside an Any field, we need to do additional work +// below. +// +// We take advantage of the fact that for the most part (with the exception of +// DynamicListener) that ConfigDump resources have a single Any field where the +// embedded resources lives. This allows us to construct an inner field mask for +// the Any resource and an outer field mask for the enclosing message. In the +// above example, the inner field mask would be "name,alt_stat_name" and the +// outer field mask "cluster,last_updated". The masks are applied to their +// respective messages, with the Any resource requiring an unpack/mask/pack +// series of operations. +// +// TODO(htuch): we could make field masks more powerful in future and generalize +// this to allow arbitrary indexing through Any fields. This is pretty +// complicated, we would need to build a FieldMask tree similar to how the C++ +// Protobuf library does this internally. +void trimResourceMessage(const Protobuf::FieldMask& field_mask, Protobuf::Message& message) { + const Protobuf::Descriptor* descriptor = message.GetDescriptor(); + const Protobuf::Reflection* reflection = message.GetReflection(); + // Figure out which paths cover Any fields. For each field, gather the paths to + // an inner mask, switch the outer mask to cover only the original field. + Protobuf::FieldMask outer_field_mask; + Protobuf::FieldMask inner_field_mask; + std::string any_field_name; + for (int i = 0; i < field_mask.paths().size(); ++i) { + const std::string& path = field_mask.paths(i); + std::vector frags = absl::StrSplit(path, '.'); + if (frags.empty()) { + continue; + } + const Protobuf::FieldDescriptor* field = descriptor->FindFieldByName(frags[0]); + // Only a single Any field supported, repeated fields don't support further + // indexing. + // TODO(htuch): should add support for DynamicListener for multiple Any + // fields in the future, see + // https://github.com/envoyproxy/envoy/issues/9669. + if (field != nullptr && field->message_type() != nullptr && !field->is_repeated() && + field->message_type()->full_name() == "google.protobuf.Any") { + if (any_field_name.empty()) { + any_field_name = frags[0]; + } else { + // This should be structurally true due to the ConfigDump proto + // definition (but not for DynamicListener today). + ASSERT(any_field_name == frags[0], + "Only a single Any field in a config dump resource is supported."); + } + outer_field_mask.add_paths(frags[0]); + frags.erase(frags.begin()); + inner_field_mask.add_paths(absl::StrJoin(frags, ".")); + } else { + outer_field_mask.add_paths(path); + } + } + + if (!any_field_name.empty()) { + const Protobuf::FieldDescriptor* any_field = descriptor->FindFieldByName(any_field_name); + if (reflection->HasField(message, any_field)) { + ASSERT(any_field != nullptr); + // Unpack to a DynamicMessage. + ProtobufWkt::Any any_message; + any_message.MergeFrom(reflection->GetMessage(message, any_field)); + Protobuf::DynamicMessageFactory dmf; + const absl::string_view inner_type_name = + TypeUtil::typeUrlToDescriptorFullName(any_message.type_url()); + const Protobuf::Descriptor* inner_descriptor = + Protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName( + static_cast(inner_type_name)); + ASSERT(inner_descriptor != nullptr); + std::unique_ptr inner_message; + inner_message.reset(dmf.GetPrototype(inner_descriptor)->New()); + MessageUtil::unpackTo(any_message, *inner_message); + // Trim message. + ProtobufUtil::FieldMaskUtil::TrimMessage(inner_field_mask, inner_message.get()); + // Pack it back into the Any resource. + any_message.PackFrom(*inner_message); + reflection->MutableMessage(&message, any_field)->CopyFrom(any_message); + } + } + ProtobufUtil::FieldMaskUtil::TrimMessage(outer_field_mask, &message); +} + +// Helper method to get the resource parameter. +absl::optional resourceParam(const Http::Utility::QueryParams& params) { + return Utility::queryParam(params, "resource"); +} + +// Helper method to get the mask parameter. +absl::optional maskParam(const Http::Utility::QueryParams& params) { + return Utility::queryParam(params, "mask"); +} + +// Helper method to get the eds parameter. +bool shouldIncludeEdsInDump(const Http::Utility::QueryParams& params) { + return Utility::queryParam(params, "include_eds") != absl::nullopt; +} + +} // namespace + +ConfigDumpHandler::ConfigDumpHandler(ConfigTracker& config_tracker, Server::Instance& server) + : HandlerContextBase(server), config_tracker_(config_tracker) {} + +Http::Code ConfigDumpHandler::handlerConfigDump(absl::string_view url, + Http::ResponseHeaderMap& response_headers, + Buffer::Instance& response, AdminStream&) const { + Http::Utility::QueryParams query_params = Http::Utility::parseAndDecodeQueryString(url); + const auto resource = resourceParam(query_params); + const auto mask = maskParam(query_params); + const bool include_eds = shouldIncludeEdsInDump(query_params); + + envoy::admin::v3::ConfigDump dump; + + if (resource.has_value()) { + auto err = addResourceToDump(dump, mask, resource.value(), include_eds); + if (err.has_value()) { + response.add(err.value().second); + return err.value().first; + } + } else { + addAllConfigToDump(dump, mask, include_eds); + } + MessageUtil::redact(dump); + + response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json); + response.add(MessageUtil::getJsonStringFromMessage(dump, true)); // pretty-print + return Http::Code::OK; +} + +absl::optional> +ConfigDumpHandler::addResourceToDump(envoy::admin::v3::ConfigDump& dump, + const absl::optional& mask, + const std::string& resource, bool include_eds) const { + Envoy::Server::ConfigTracker::CbsMap callbacks_map = config_tracker_.getCallbacksMap(); + if (include_eds) { + if (!server_.clusterManager().clusters().empty()) { + callbacks_map.emplace("endpoint", [this] { return dumpEndpointConfigs(); }); + } + } + + for (const auto& [name, callback] : callbacks_map) { + ProtobufTypes::MessagePtr message = callback(); + ASSERT(message); + + auto field_descriptor = message->GetDescriptor()->FindFieldByName(resource); + const Protobuf::Reflection* reflection = message->GetReflection(); + if (!field_descriptor) { + continue; + } else if (!field_descriptor->is_repeated()) { + return absl::optional>{std::make_pair( + Http::Code::BadRequest, + fmt::format("{} is not a repeated field. Use ?mask={} to get only this field", + field_descriptor->name(), field_descriptor->name()))}; + } + + auto repeated = reflection->GetRepeatedPtrField(*message, field_descriptor); + for (Protobuf::Message& msg : repeated) { + if (mask.has_value()) { + Protobuf::FieldMask field_mask; + ProtobufUtil::FieldMaskUtil::FromString(mask.value(), &field_mask); + trimResourceMessage(field_mask, msg); + } + auto* config = dump.add_configs(); + config->PackFrom(msg); + } + + // We found the desired resource so there is no need to continue iterating over + // the other keys. + return absl::nullopt; + } + + return absl::optional>{ + std::make_pair(Http::Code::NotFound, fmt::format("{} not found in config dump", resource))}; +} + +void ConfigDumpHandler::addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, + const absl::optional& mask, + bool include_eds) const { + Envoy::Server::ConfigTracker::CbsMap callbacks_map = config_tracker_.getCallbacksMap(); + if (include_eds) { + if (!server_.clusterManager().clusters().empty()) { + callbacks_map.emplace("endpoint", [this] { return dumpEndpointConfigs(); }); + } + } + + for (const auto& [name, callback] : callbacks_map) { + ProtobufTypes::MessagePtr message = callback(); + ASSERT(message); + + if (mask.has_value()) { + Protobuf::FieldMask field_mask; + ProtobufUtil::FieldMaskUtil::FromString(mask.value(), &field_mask); + // We don't use trimMessage() above here since masks don't support + // indexing through repeated fields. + ProtobufUtil::FieldMaskUtil::TrimMessage(field_mask, message.get()); + } + + auto* config = dump.add_configs(); + config->PackFrom(*message); + } +} + +ProtobufTypes::MessagePtr ConfigDumpHandler::dumpEndpointConfigs() const { + auto endpoint_config_dump = std::make_unique(); + + for (const auto& [name, cluster_ref] : server_.clusterManager().clusters()) { + const Upstream::Cluster& cluster = cluster_ref.get(); + Upstream::ClusterInfoConstSharedPtr cluster_info = cluster.info(); + envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; + + if (cluster_info->edsServiceName().has_value()) { + cluster_load_assignment.set_cluster_name(cluster_info->edsServiceName().value()); + } else { + cluster_load_assignment.set_cluster_name(cluster_info->name()); + } + auto& policy = *cluster_load_assignment.mutable_policy(); + + for (auto& host_set : cluster.prioritySet().hostSetsPerPriority()) { + policy.mutable_overprovisioning_factor()->set_value(host_set->overprovisioningFactor()); + + if (!host_set->hostsPerLocality().get().empty()) { + for (int index = 0; index < static_cast(host_set->hostsPerLocality().get().size()); + index++) { + auto locality_host_set = host_set->hostsPerLocality().get()[index]; + + if (!locality_host_set.empty()) { + auto& locality_lb_endpoint = *cluster_load_assignment.mutable_endpoints()->Add(); + locality_lb_endpoint.mutable_locality()->MergeFrom(locality_host_set[0]->locality()); + locality_lb_endpoint.set_priority(locality_host_set[0]->priority()); + if (host_set->localityWeights() != nullptr && !host_set->localityWeights()->empty()) { + locality_lb_endpoint.mutable_load_balancing_weight()->set_value( + (*host_set->localityWeights())[index]); + } + + for (auto& host : locality_host_set) { + addLbEndpoint(host, locality_lb_endpoint); + } + } + } + } else { + for (auto& host : host_set->hosts()) { + auto& locality_lb_endpoint = *cluster_load_assignment.mutable_endpoints()->Add(); + locality_lb_endpoint.mutable_locality()->MergeFrom(host->locality()); + locality_lb_endpoint.set_priority(host->priority()); + addLbEndpoint(host, locality_lb_endpoint); + } + } + } + + if (cluster_info->addedViaApi()) { + auto& dynamic_endpoint = *endpoint_config_dump->mutable_dynamic_endpoint_configs()->Add(); + dynamic_endpoint.mutable_endpoint_config()->PackFrom(cluster_load_assignment); + } else { + auto& static_endpoint = *endpoint_config_dump->mutable_static_endpoint_configs()->Add(); + static_endpoint.mutable_endpoint_config()->PackFrom(cluster_load_assignment); + } + } + return endpoint_config_dump; +} + +void ConfigDumpHandler::addLbEndpoint( + const Upstream::HostSharedPtr& host, + envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint) const { + auto& lb_endpoint = *locality_lb_endpoint.mutable_lb_endpoints()->Add(); + if (host->metadata() != nullptr) { + lb_endpoint.mutable_metadata()->MergeFrom(*host->metadata()); + } + lb_endpoint.mutable_load_balancing_weight()->set_value(host->weight()); + + switch (host->health()) { + case Upstream::Host::Health::Healthy: + lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::HEALTHY); + break; + case Upstream::Host::Health::Unhealthy: + lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::UNHEALTHY); + break; + case Upstream::Host::Health::Degraded: + lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::DEGRADED); + break; + default: + lb_endpoint.set_health_status(envoy::config::core::v3::HealthStatus::UNKNOWN); + } + + auto& endpoint = *lb_endpoint.mutable_endpoint(); + endpoint.set_hostname(host->hostname()); + Network::Utility::addressToProtobufAddress(*host->address(), *endpoint.mutable_address()); + auto& health_check_config = *endpoint.mutable_health_check_config(); + health_check_config.set_hostname(host->hostnameForHealthChecks()); + if (host->healthCheckAddress()->asString() != host->address()->asString()) { + health_check_config.set_port_value(host->healthCheckAddress()->ip()->port()); + } +} + +} // namespace Server +} // namespace Envoy diff --git a/source/server/admin/config_dump_handler.h b/source/server/admin/config_dump_handler.h new file mode 100644 index 0000000000000..f8b77c4a2e6fb --- /dev/null +++ b/source/server/admin/config_dump_handler.h @@ -0,0 +1,53 @@ + +#pragma once + +#include "envoy/admin/v3/config_dump.pb.h" +#include "envoy/buffer/buffer.h" +#include "envoy/config/endpoint/v3/endpoint_components.pb.h" +#include "envoy/http/codes.h" +#include "envoy/http/header_map.h" +#include "envoy/server/admin.h" +#include "envoy/server/instance.h" + +#include "server/admin/config_tracker_impl.h" +#include "server/admin/handler_ctx.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Server { + +class ConfigDumpHandler : public HandlerContextBase { + +public: + ConfigDumpHandler(ConfigTracker& config_tracker, Server::Instance& server); + + Http::Code handlerConfigDump(absl::string_view path_and_query, + Http::ResponseHeaderMap& response_headers, + Buffer::Instance& response, AdminStream&) const; + +private: + void addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, + const absl::optional& mask, bool include_eds) const; + /** + * Add the config matching the passed resource to the passed config dump. + * @return absl::nullopt on success, else the Http::Code and an error message that should be added + * to the admin response. + */ + absl::optional> + addResourceToDump(envoy::admin::v3::ConfigDump& dump, const absl::optional& mask, + const std::string& resource, bool include_eds) const; + + /** + * Helper methods to add endpoints config + */ + void addLbEndpoint(const Upstream::HostSharedPtr& host, + envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint) const; + + ProtobufTypes::MessagePtr dumpEndpointConfigs() const; + + ConfigTracker& config_tracker_; +}; + +} // namespace Server +} // namespace Envoy diff --git a/test/server/admin/BUILD b/test/server/admin/BUILD index ef71496edaf50..e2e036bbc833f 100644 --- a/test/server/admin/BUILD +++ b/test/server/admin/BUILD @@ -42,10 +42,6 @@ envoy_cc_test( "//test/test_common:logging_lib", "//test/test_common:network_utility_lib", "//test/test_common:utility_lib", - "@envoy_api//envoy/admin/v3:pkg_cc_proto", - "@envoy_api//envoy/config/core/v3:pkg_cc_proto", - "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", - "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", ], ) @@ -124,6 +120,14 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "config_dump_handler_test", + srcs = ["config_dump_handler_test.cc"], + deps = [ + ":admin_instance_lib", + ], +) + envoy_cc_test( name = "config_tracker_impl_test", srcs = ["config_tracker_impl_test.cc"], diff --git a/test/server/admin/admin_test.cc b/test/server/admin/admin_test.cc index 1edd4beef5910..3901262c7001e 100644 --- a/test/server/admin/admin_test.cc +++ b/test/server/admin/admin_test.cc @@ -4,11 +4,6 @@ #include #include -#include "envoy/admin/v3/config_dump.pb.h" -#include "envoy/admin/v3/server_info.pb.h" -#include "envoy/config/core/v3/base.pb.h" -#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" -#include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" #include "envoy/json/json_object.h" #include "envoy/upstream/outlier_detection.h" #include "envoy/upstream/upstream.h" @@ -145,605 +140,5 @@ TEST_P(AdminInstanceTest, HelpUsesFormForMutations) { EXPECT_NE(-1, response.search(stats_href.data(), stats_href.size(), 0, 0)); } -TEST_P(AdminInstanceTest, ConfigDump) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto entry = admin_.getConfigTracker().add("foo", [] { - auto msg = std::make_unique(); - msg->set_value("bar"); - return msg; - }); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": "bar" - } - ] -} -)EOF"; - EXPECT_EQ(Http::Code::OK, getCallback("/config_dump", header_map, response)); - std::string output = response.toString(); - EXPECT_EQ(expected_json, output); -} - -TEST_P(AdminInstanceTest, ConfigDumpMaintainsOrder) { - // Add configs in random order and validate config_dump dumps in the order. - auto bootstrap_entry = admin_.getConfigTracker().add("bootstrap", [] { - auto msg = std::make_unique(); - msg->set_value("bootstrap_config"); - return msg; - }); - auto route_entry = admin_.getConfigTracker().add("routes", [] { - auto msg = std::make_unique(); - msg->set_value("routes_config"); - return msg; - }); - auto listener_entry = admin_.getConfigTracker().add("listeners", [] { - auto msg = std::make_unique(); - msg->set_value("listeners_config"); - return msg; - }); - auto cluster_entry = admin_.getConfigTracker().add("clusters", [] { - auto msg = std::make_unique(); - msg->set_value("clusters_config"); - return msg; - }); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": "bootstrap_config" - }, - { - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": "clusters_config" - }, - { - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": "listeners_config" - }, - { - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": "routes_config" - } - ] -} -)EOF"; - // Run it multiple times and validate that order is preserved. - for (size_t i = 0; i < 5; i++) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - EXPECT_EQ(Http::Code::OK, getCallback("/config_dump", header_map, response)); - const std::string output = response.toString(); - EXPECT_EQ(expected_json, output); - } -} - -// helper method for adding host's info -void addHostInfo(NiceMock& host, const std::string& hostname, - const std::string& address_url, envoy::config::core::v3::Locality& locality, - const std::string& hostname_for_healthcheck, - const std::string& healthcheck_address_url, int weight, int priority) { - ON_CALL(host, locality()).WillByDefault(ReturnRef(locality)); - - Network::Address::InstanceConstSharedPtr address = Network::Utility::resolveUrl(address_url); - ON_CALL(host, address()).WillByDefault(Return(address)); - ON_CALL(host, hostname()).WillByDefault(ReturnRef(hostname)); - - ON_CALL(host, hostnameForHealthChecks()).WillByDefault(ReturnRef(hostname_for_healthcheck)); - Network::Address::InstanceConstSharedPtr healthcheck_address = - Network::Utility::resolveUrl(healthcheck_address_url); - ON_CALL(host, healthCheckAddress()).WillByDefault(Return(healthcheck_address)); - - auto metadata = std::make_shared(); - ON_CALL(host, metadata()).WillByDefault(Return(metadata)); - - ON_CALL(host, health()).WillByDefault(Return(Upstream::Host::Health::Healthy)); - - ON_CALL(host, weight()).WillByDefault(Return(weight)); - ON_CALL(host, priority()).WillByDefault(Return(priority)); -} - -// Test that using ?include_eds parameter adds EDS to the config dump. -TEST_P(AdminInstanceTest, ConfigDumpWithEndpoint) { - Upstream::ClusterManager::ClusterInfoMap cluster_map; - ON_CALL(server_.cluster_manager_, clusters()).WillByDefault(ReturnPointee(&cluster_map)); - - NiceMock cluster; - cluster_map.emplace(cluster.info_->name_, cluster); - - ON_CALL(*cluster.info_, addedViaApi()).WillByDefault(Return(false)); - - Upstream::MockHostSet* host_set = cluster.priority_set_.getMockHostSet(0); - auto host = std::make_shared>(); - host_set->hosts_.emplace_back(host); - - envoy::config::core::v3::Locality locality; - const std::string hostname_for_healthcheck = "test_hostname_healthcheck"; - const std::string hostname = "foo.com"; - - addHostInfo(*host, hostname, "tcp://1.2.3.4:80", locality, hostname_for_healthcheck, - "tcp://1.2.3.5:90", 5, 6); - - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?include_eds", header_map, response)); - std::string output = response.toString(); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", - "static_endpoint_configs": [ - { - "endpoint_config": { - "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "cluster_name": "fake_cluster", - "endpoints": [ - { - "locality": {}, - "lb_endpoints": [ - { - "endpoint": { - "address": { - "socket_address": { - "address": "1.2.3.4", - "port_value": 80 - } - }, - "health_check_config": { - "port_value": 90, - "hostname": "test_hostname_healthcheck" - }, - "hostname": "foo.com" - }, - "health_status": "HEALTHY", - "metadata": {}, - "load_balancing_weight": 5 - } - ], - "priority": 6 - } - ], - "policy": { - "overprovisioning_factor": 140 - } - } - } - ] - } - ] -} -)EOF"; - EXPECT_EQ(expected_json, output); -} - -// Test EDS config dump while multiple localities and priorities exist -TEST_P(AdminInstanceTest, ConfigDumpWithLocalityEndpoint) { - Upstream::ClusterManager::ClusterInfoMap cluster_map; - ON_CALL(server_.cluster_manager_, clusters()).WillByDefault(ReturnPointee(&cluster_map)); - - NiceMock cluster; - cluster_map.emplace(cluster.info_->name_, cluster); - - ON_CALL(*cluster.info_, addedViaApi()).WillByDefault(Return(false)); - - Upstream::MockHostSet* host_set_1 = cluster.priority_set_.getMockHostSet(0); - auto host_1 = std::make_shared>(); - host_set_1->hosts_.emplace_back(host_1); - - envoy::config::core::v3::Locality locality_1; - locality_1.set_region("oceania"); - locality_1.set_zone("hello"); - locality_1.set_sub_zone("world"); - - const std::string hostname_for_healthcheck = "test_hostname_healthcheck"; - const std::string hostname_1 = "foo.com"; - - addHostInfo(*host_1, hostname_1, "tcp://1.2.3.4:80", locality_1, hostname_for_healthcheck, - "tcp://1.2.3.5:90", 5, 6); - - auto host_2 = std::make_shared>(); - host_set_1->hosts_.emplace_back(host_2); - const std::string empty_hostname_for_healthcheck = ""; - const std::string hostname_2 = "boo.com"; - - addHostInfo(*host_2, hostname_2, "tcp://1.2.3.7:8", locality_1, empty_hostname_for_healthcheck, - "tcp://1.2.3.7:8", 3, 6); - - envoy::config::core::v3::Locality locality_2; - - auto host_3 = std::make_shared>(); - host_set_1->hosts_.emplace_back(host_3); - const std::string hostname_3 = "coo.com"; - - addHostInfo(*host_3, hostname_3, "tcp://1.2.3.8:8", locality_2, empty_hostname_for_healthcheck, - "tcp://1.2.3.8:8", 3, 4); - - std::vector locality_hosts = { - {Upstream::HostSharedPtr(host_1), Upstream::HostSharedPtr(host_2)}, - {Upstream::HostSharedPtr(host_3)}}; - auto hosts_per_locality = new Upstream::HostsPerLocalityImpl(std::move(locality_hosts), false); - - Upstream::LocalityWeightsConstSharedPtr locality_weights{new Upstream::LocalityWeights{1, 3}}; - ON_CALL(*host_set_1, hostsPerLocality()).WillByDefault(ReturnRef(*hosts_per_locality)); - ON_CALL(*host_set_1, localityWeights()).WillByDefault(Return(locality_weights)); - - Upstream::MockHostSet* host_set_2 = cluster.priority_set_.getMockHostSet(1); - auto host_4 = std::make_shared>(); - host_set_2->hosts_.emplace_back(host_4); - const std::string hostname_4 = "doo.com"; - - addHostInfo(*host_4, hostname_4, "tcp://1.2.3.9:8", locality_1, empty_hostname_for_healthcheck, - "tcp://1.2.3.9:8", 3, 2); - - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?include_eds", header_map, response)); - std::string output = response.toString(); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", - "static_endpoint_configs": [ - { - "endpoint_config": { - "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "cluster_name": "fake_cluster", - "endpoints": [ - { - "locality": { - "region": "oceania", - "zone": "hello", - "sub_zone": "world" - }, - "lb_endpoints": [ - { - "endpoint": { - "address": { - "socket_address": { - "address": "1.2.3.4", - "port_value": 80 - } - }, - "health_check_config": { - "port_value": 90, - "hostname": "test_hostname_healthcheck" - }, - "hostname": "foo.com" - }, - "health_status": "HEALTHY", - "metadata": {}, - "load_balancing_weight": 5 - }, - { - "endpoint": { - "address": { - "socket_address": { - "address": "1.2.3.7", - "port_value": 8 - } - }, - "health_check_config": {}, - "hostname": "boo.com" - }, - "health_status": "HEALTHY", - "metadata": {}, - "load_balancing_weight": 3 - } - ], - "load_balancing_weight": 1, - "priority": 6 - }, - { - "locality": {}, - "lb_endpoints": [ - { - "endpoint": { - "address": { - "socket_address": { - "address": "1.2.3.8", - "port_value": 8 - } - }, - "health_check_config": {}, - "hostname": "coo.com" - }, - "health_status": "HEALTHY", - "metadata": {}, - "load_balancing_weight": 3 - } - ], - "load_balancing_weight": 3, - "priority": 4 - }, - { - "locality": { - "region": "oceania", - "zone": "hello", - "sub_zone": "world" - }, - "lb_endpoints": [ - { - "endpoint": { - "address": { - "socket_address": { - "address": "1.2.3.9", - "port_value": 8 - } - }, - "health_check_config": {}, - "hostname": "doo.com" - }, - "health_status": "HEALTHY", - "metadata": {}, - "load_balancing_weight": 3 - } - ], - "priority": 2 - } - ], - "policy": { - "overprovisioning_factor": 140 - } - } - } - ] - } - ] -} -)EOF"; - EXPECT_EQ(expected_json, output); - delete (hosts_per_locality); -} - -// Test that using the resource query parameter filters the config dump. -// We add both static and dynamic listener config to the dump, but expect only -// dynamic in the JSON with ?resource=dynamic_listeners. -TEST_P(AdminInstanceTest, ConfigDumpFiltersByResource) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto listeners = admin_.getConfigTracker().add("listeners", [] { - auto msg = std::make_unique(); - auto dyn_listener = msg->add_dynamic_listeners(); - dyn_listener->set_name("foo"); - auto stat_listener = msg->add_static_listeners(); - envoy::config::listener::v3::Listener listener; - listener.set_name("bar"); - stat_listener->mutable_listener()->PackFrom(listener); - return msg; - }); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener", - "name": "foo" - } - ] -} -)EOF"; - EXPECT_EQ(Http::Code::OK, - getCallback("/config_dump?resource=dynamic_listeners", header_map, response)); - std::string output = response.toString(); - EXPECT_EQ(expected_json, output); -} - -// Test that using the resource query parameter filters the config dump including EDS. -// We add both static and dynamic endpoint config to the dump, but expect only -// dynamic in the JSON with ?resource=dynamic_endpoint_configs. -TEST_P(AdminInstanceTest, ConfigDumpWithEndpointFiltersByResource) { - Upstream::ClusterManager::ClusterInfoMap cluster_map; - ON_CALL(server_.cluster_manager_, clusters()).WillByDefault(ReturnPointee(&cluster_map)); - - NiceMock cluster_1; - cluster_map.emplace(cluster_1.info_->name_, cluster_1); - - ON_CALL(*cluster_1.info_, addedViaApi()).WillByDefault(Return(true)); - - Upstream::MockHostSet* host_set = cluster_1.priority_set_.getMockHostSet(0); - auto host_1 = std::make_shared>(); - host_set->hosts_.emplace_back(host_1); - - envoy::config::core::v3::Locality locality; - const std::string hostname_for_healthcheck = "test_hostname_healthcheck"; - const std::string hostname_1 = "foo.com"; - - addHostInfo(*host_1, hostname_1, "tcp://1.2.3.4:80", locality, hostname_for_healthcheck, - "tcp://1.2.3.5:90", 5, 6); - - NiceMock cluster_2; - cluster_2.info_->name_ = "fake_cluster_2"; - cluster_map.emplace(cluster_2.info_->name_, cluster_2); - - ON_CALL(*cluster_2.info_, addedViaApi()).WillByDefault(Return(false)); - - Upstream::MockHostSet* host_set_2 = cluster_2.priority_set_.getMockHostSet(0); - auto host_2 = std::make_shared>(); - host_set_2->hosts_.emplace_back(host_2); - const std::string hostname_2 = "boo.com"; - - addHostInfo(*host_2, hostname_2, "tcp://1.2.3.5:8", locality, hostname_for_healthcheck, - "tcp://1.2.3.4:1", 3, 4); - - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - EXPECT_EQ(Http::Code::OK, - getCallback("/config_dump?include_eds&resource=dynamic_endpoint_configs", header_map, - response)); - std::string output = response.toString(); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump.DynamicEndpointConfig", - "endpoint_config": { - "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "cluster_name": "fake_cluster", - "endpoints": [ - { - "locality": {}, - "lb_endpoints": [ - { - "endpoint": { - "address": { - "socket_address": { - "address": "1.2.3.4", - "port_value": 80 - } - }, - "health_check_config": { - "port_value": 90, - "hostname": "test_hostname_healthcheck" - }, - "hostname": "foo.com" - }, - "health_status": "HEALTHY", - "metadata": {}, - "load_balancing_weight": 5 - } - ], - "priority": 6 - } - ], - "policy": { - "overprovisioning_factor": 140 - } - } - } - ] -} -)EOF"; - EXPECT_EQ(expected_json, output); -} - -// Test that using the mask query parameter filters the config dump. -// We add both static and dynamic listener config to the dump, but expect only -// dynamic in the JSON with ?mask=dynamic_listeners. -TEST_P(AdminInstanceTest, ConfigDumpFiltersByMask) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto listeners = admin_.getConfigTracker().add("listeners", [] { - auto msg = std::make_unique(); - auto dyn_listener = msg->add_dynamic_listeners(); - dyn_listener->set_name("foo"); - auto stat_listener = msg->add_static_listeners(); - envoy::config::listener::v3::Listener listener; - listener.set_name("bar"); - stat_listener->mutable_listener()->PackFrom(listener); - return msg; - }); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump", - "dynamic_listeners": [ - { - "name": "foo" - } - ] - } - ] -} -)EOF"; - EXPECT_EQ(Http::Code::OK, - getCallback("/config_dump?mask=dynamic_listeners", header_map, response)); - std::string output = response.toString(); - EXPECT_EQ(expected_json, output); -} - -ProtobufTypes::MessagePtr testDumpClustersConfig() { - auto msg = std::make_unique(); - auto* static_cluster = msg->add_static_clusters(); - envoy::config::cluster::v3::Cluster inner_cluster; - inner_cluster.set_name("foo"); - inner_cluster.set_ignore_health_on_host_removal(true); - static_cluster->mutable_cluster()->PackFrom(inner_cluster); - - auto* dyn_cluster = msg->add_dynamic_active_clusters(); - dyn_cluster->set_version_info("baz"); - dyn_cluster->mutable_last_updated()->set_seconds(5); - envoy::config::cluster::v3::Cluster inner_dyn_cluster; - inner_dyn_cluster.set_name("bar"); - inner_dyn_cluster.set_ignore_health_on_host_removal(true); - inner_dyn_cluster.mutable_http2_protocol_options()->set_allow_connect(true); - dyn_cluster->mutable_cluster()->PackFrom(inner_dyn_cluster); - return msg; -} - -// Test that when using both resource and mask query parameters the JSON output contains -// only the desired resource and the fields specified in the mask. -TEST_P(AdminInstanceTest, ConfigDumpFiltersByResourceAndMask) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto clusters = admin_.getConfigTracker().add("clusters", testDumpClustersConfig); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster", - "version_info": "baz", - "cluster": { - "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", - "name": "bar", - "http2_protocol_options": { - "allow_connect": true - } - } - } - ] -} -)EOF"; - EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?resource=dynamic_active_clusters&mask=" - "cluster.name,version_info,cluster.http2_protocol_options", - header_map, response)); - std::string output = response.toString(); - EXPECT_EQ(expected_json, output); -} - -// Test that no fields are present in the JSON output if there is no intersection between the fields -// of the config dump and the fields present in the mask query parameter. -TEST_P(AdminInstanceTest, ConfigDumpNonExistentMask) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto clusters = admin_.getConfigTracker().add("clusters", testDumpClustersConfig); - const std::string expected_json = R"EOF({ - "configs": [ - { - "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump.StaticCluster" - } - ] -} -)EOF"; - EXPECT_EQ(Http::Code::OK, - getCallback("/config_dump?resource=static_clusters&mask=bad", header_map, response)); - std::string output = response.toString(); - EXPECT_EQ(expected_json, output); -} - -// Test that a 404 Not found is returned if a non-existent resource is passed in as the -// resource query parameter. -TEST_P(AdminInstanceTest, ConfigDumpNonExistentResource) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto listeners = admin_.getConfigTracker().add("listeners", [] { - auto msg = std::make_unique(); - msg->set_value("listeners_config"); - return msg; - }); - EXPECT_EQ(Http::Code::NotFound, getCallback("/config_dump?resource=foo", header_map, response)); -} - -// Test that a 400 Bad Request is returned if the passed resource query parameter is not a -// repeated field. -TEST_P(AdminInstanceTest, ConfigDumpResourceNotRepeated) { - Buffer::OwnedImpl response; - Http::TestResponseHeaderMapImpl header_map; - auto clusters = admin_.getConfigTracker().add("clusters", [] { - auto msg = std::make_unique(); - msg->set_version_info("foo"); - return msg; - }); - EXPECT_EQ(Http::Code::BadRequest, - getCallback("/config_dump?resource=version_info", header_map, response)); -} - } // namespace Server } // namespace Envoy diff --git a/test/server/admin/config_dump_handler_test.cc b/test/server/admin/config_dump_handler_test.cc new file mode 100644 index 0000000000000..7c7f5f57781f8 --- /dev/null +++ b/test/server/admin/config_dump_handler_test.cc @@ -0,0 +1,615 @@ +#include "test/server/admin/admin_instance.h" + +using testing::Return; +using testing::ReturnPointee; +using testing::ReturnRef; + +namespace Envoy { +namespace Server { + +INSTANTIATE_TEST_SUITE_P(IpVersions, AdminInstanceTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// helper method for adding host's info +void addHostInfo(NiceMock& host, const std::string& hostname, + const std::string& address_url, envoy::config::core::v3::Locality& locality, + const std::string& hostname_for_healthcheck, + const std::string& healthcheck_address_url, int weight, int priority) { + ON_CALL(host, locality()).WillByDefault(ReturnRef(locality)); + + Network::Address::InstanceConstSharedPtr address = Network::Utility::resolveUrl(address_url); + ON_CALL(host, address()).WillByDefault(Return(address)); + ON_CALL(host, hostname()).WillByDefault(ReturnRef(hostname)); + + ON_CALL(host, hostnameForHealthChecks()).WillByDefault(ReturnRef(hostname_for_healthcheck)); + Network::Address::InstanceConstSharedPtr healthcheck_address = + Network::Utility::resolveUrl(healthcheck_address_url); + ON_CALL(host, healthCheckAddress()).WillByDefault(Return(healthcheck_address)); + + auto metadata = std::make_shared(); + ON_CALL(host, metadata()).WillByDefault(Return(metadata)); + + ON_CALL(host, health()).WillByDefault(Return(Upstream::Host::Health::Healthy)); + + ON_CALL(host, weight()).WillByDefault(Return(weight)); + ON_CALL(host, priority()).WillByDefault(Return(priority)); +} + +TEST_P(AdminInstanceTest, ConfigDump) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto entry = admin_.getConfigTracker().add("foo", [] { + auto msg = std::make_unique(); + msg->set_value("bar"); + return msg; + }); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "bar" + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, getCallback("/config_dump", header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + +TEST_P(AdminInstanceTest, ConfigDumpMaintainsOrder) { + // Add configs in random order and validate config_dump dumps in the order. + auto bootstrap_entry = admin_.getConfigTracker().add("bootstrap", [] { + auto msg = std::make_unique(); + msg->set_value("bootstrap_config"); + return msg; + }); + auto route_entry = admin_.getConfigTracker().add("routes", [] { + auto msg = std::make_unique(); + msg->set_value("routes_config"); + return msg; + }); + auto listener_entry = admin_.getConfigTracker().add("listeners", [] { + auto msg = std::make_unique(); + msg->set_value("listeners_config"); + return msg; + }); + auto cluster_entry = admin_.getConfigTracker().add("clusters", [] { + auto msg = std::make_unique(); + msg->set_value("clusters_config"); + return msg; + }); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "bootstrap_config" + }, + { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "clusters_config" + }, + { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "listeners_config" + }, + { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "routes_config" + } + ] +} +)EOF"; + // Run it multiple times and validate that order is preserved. + for (size_t i = 0; i < 5; i++) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + EXPECT_EQ(Http::Code::OK, getCallback("/config_dump", header_map, response)); + const std::string output = response.toString(); + EXPECT_EQ(expected_json, output); + } +} + +// Test that using ?include_eds parameter adds EDS to the config dump. +TEST_P(AdminInstanceTest, ConfigDumpWithEndpoint) { + Upstream::ClusterManager::ClusterInfoMap cluster_map; + ON_CALL(server_.cluster_manager_, clusters()).WillByDefault(ReturnPointee(&cluster_map)); + + NiceMock cluster; + cluster_map.emplace(cluster.info_->name_, cluster); + + ON_CALL(*cluster.info_, addedViaApi()).WillByDefault(Return(false)); + + Upstream::MockHostSet* host_set = cluster.priority_set_.getMockHostSet(0); + auto host = std::make_shared>(); + host_set->hosts_.emplace_back(host); + + envoy::config::core::v3::Locality locality; + const std::string hostname_for_healthcheck = "test_hostname_healthcheck"; + const std::string hostname = "foo.com"; + + addHostInfo(*host, hostname, "tcp://1.2.3.4:80", locality, hostname_for_healthcheck, + "tcp://1.2.3.5:90", 5, 6); + + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?include_eds", header_map, response)); + std::string output = response.toString(); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", + "static_endpoint_configs": [ + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "cluster_name": "fake_cluster", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "1.2.3.4", + "port_value": 80 + } + }, + "health_check_config": { + "port_value": 90, + "hostname": "test_hostname_healthcheck" + }, + "hostname": "foo.com" + }, + "health_status": "HEALTHY", + "metadata": {}, + "load_balancing_weight": 5 + } + ], + "priority": 6 + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + } + ] + } + ] +} +)EOF"; + EXPECT_EQ(expected_json, output); +} + +// Test EDS config dump while multiple localities and priorities exist +TEST_P(AdminInstanceTest, ConfigDumpWithLocalityEndpoint) { + Upstream::ClusterManager::ClusterInfoMap cluster_map; + ON_CALL(server_.cluster_manager_, clusters()).WillByDefault(ReturnPointee(&cluster_map)); + + NiceMock cluster; + cluster_map.emplace(cluster.info_->name_, cluster); + + ON_CALL(*cluster.info_, addedViaApi()).WillByDefault(Return(false)); + + Upstream::MockHostSet* host_set_1 = cluster.priority_set_.getMockHostSet(0); + auto host_1 = std::make_shared>(); + host_set_1->hosts_.emplace_back(host_1); + + envoy::config::core::v3::Locality locality_1; + locality_1.set_region("oceania"); + locality_1.set_zone("hello"); + locality_1.set_sub_zone("world"); + + const std::string hostname_for_healthcheck = "test_hostname_healthcheck"; + const std::string hostname_1 = "foo.com"; + + addHostInfo(*host_1, hostname_1, "tcp://1.2.3.4:80", locality_1, hostname_for_healthcheck, + "tcp://1.2.3.5:90", 5, 6); + + auto host_2 = std::make_shared>(); + host_set_1->hosts_.emplace_back(host_2); + const std::string empty_hostname_for_healthcheck = ""; + const std::string hostname_2 = "boo.com"; + + addHostInfo(*host_2, hostname_2, "tcp://1.2.3.7:8", locality_1, empty_hostname_for_healthcheck, + "tcp://1.2.3.7:8", 3, 6); + + envoy::config::core::v3::Locality locality_2; + + auto host_3 = std::make_shared>(); + host_set_1->hosts_.emplace_back(host_3); + const std::string hostname_3 = "coo.com"; + + addHostInfo(*host_3, hostname_3, "tcp://1.2.3.8:8", locality_2, empty_hostname_for_healthcheck, + "tcp://1.2.3.8:8", 3, 4); + + std::vector locality_hosts = { + {Upstream::HostSharedPtr(host_1), Upstream::HostSharedPtr(host_2)}, + {Upstream::HostSharedPtr(host_3)}}; + auto hosts_per_locality = new Upstream::HostsPerLocalityImpl(std::move(locality_hosts), false); + + Upstream::LocalityWeightsConstSharedPtr locality_weights{new Upstream::LocalityWeights{1, 3}}; + ON_CALL(*host_set_1, hostsPerLocality()).WillByDefault(ReturnRef(*hosts_per_locality)); + ON_CALL(*host_set_1, localityWeights()).WillByDefault(Return(locality_weights)); + + Upstream::MockHostSet* host_set_2 = cluster.priority_set_.getMockHostSet(1); + auto host_4 = std::make_shared>(); + host_set_2->hosts_.emplace_back(host_4); + const std::string hostname_4 = "doo.com"; + + addHostInfo(*host_4, hostname_4, "tcp://1.2.3.9:8", locality_1, empty_hostname_for_healthcheck, + "tcp://1.2.3.9:8", 3, 2); + + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?include_eds", header_map, response)); + std::string output = response.toString(); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", + "static_endpoint_configs": [ + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "cluster_name": "fake_cluster", + "endpoints": [ + { + "locality": { + "region": "oceania", + "zone": "hello", + "sub_zone": "world" + }, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "1.2.3.4", + "port_value": 80 + } + }, + "health_check_config": { + "port_value": 90, + "hostname": "test_hostname_healthcheck" + }, + "hostname": "foo.com" + }, + "health_status": "HEALTHY", + "metadata": {}, + "load_balancing_weight": 5 + }, + { + "endpoint": { + "address": { + "socket_address": { + "address": "1.2.3.7", + "port_value": 8 + } + }, + "health_check_config": {}, + "hostname": "boo.com" + }, + "health_status": "HEALTHY", + "metadata": {}, + "load_balancing_weight": 3 + } + ], + "load_balancing_weight": 1, + "priority": 6 + }, + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "1.2.3.8", + "port_value": 8 + } + }, + "health_check_config": {}, + "hostname": "coo.com" + }, + "health_status": "HEALTHY", + "metadata": {}, + "load_balancing_weight": 3 + } + ], + "load_balancing_weight": 3, + "priority": 4 + }, + { + "locality": { + "region": "oceania", + "zone": "hello", + "sub_zone": "world" + }, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "1.2.3.9", + "port_value": 8 + } + }, + "health_check_config": {}, + "hostname": "doo.com" + }, + "health_status": "HEALTHY", + "metadata": {}, + "load_balancing_weight": 3 + } + ], + "priority": 2 + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + } + ] + } + ] +} +)EOF"; + EXPECT_EQ(expected_json, output); + delete (hosts_per_locality); +} + +// Test that using the resource query parameter filters the config dump. +// We add both static and dynamic listener config to the dump, but expect only +// dynamic in the JSON with ?resource=dynamic_listeners. +TEST_P(AdminInstanceTest, ConfigDumpFiltersByResource) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto listeners = admin_.getConfigTracker().add("listeners", [] { + auto msg = std::make_unique(); + auto dyn_listener = msg->add_dynamic_listeners(); + dyn_listener->set_name("foo"); + auto stat_listener = msg->add_static_listeners(); + envoy::config::listener::v3::Listener listener; + listener.set_name("bar"); + stat_listener->mutable_listener()->PackFrom(listener); + return msg; + }); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener", + "name": "foo" + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, + getCallback("/config_dump?resource=dynamic_listeners", header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + +// Test that using the resource query parameter filters the config dump including EDS. +// We add both static and dynamic endpoint config to the dump, but expect only +// dynamic in the JSON with ?resource=dynamic_endpoint_configs. +TEST_P(AdminInstanceTest, ConfigDumpWithEndpointFiltersByResource) { + Upstream::ClusterManager::ClusterInfoMap cluster_map; + ON_CALL(server_.cluster_manager_, clusters()).WillByDefault(ReturnPointee(&cluster_map)); + + NiceMock cluster_1; + cluster_map.emplace(cluster_1.info_->name_, cluster_1); + + ON_CALL(*cluster_1.info_, addedViaApi()).WillByDefault(Return(true)); + + Upstream::MockHostSet* host_set = cluster_1.priority_set_.getMockHostSet(0); + auto host_1 = std::make_shared>(); + host_set->hosts_.emplace_back(host_1); + + envoy::config::core::v3::Locality locality; + const std::string hostname_for_healthcheck = "test_hostname_healthcheck"; + const std::string hostname_1 = "foo.com"; + + addHostInfo(*host_1, hostname_1, "tcp://1.2.3.4:80", locality, hostname_for_healthcheck, + "tcp://1.2.3.5:90", 5, 6); + + NiceMock cluster_2; + cluster_2.info_->name_ = "fake_cluster_2"; + cluster_map.emplace(cluster_2.info_->name_, cluster_2); + + ON_CALL(*cluster_2.info_, addedViaApi()).WillByDefault(Return(false)); + + Upstream::MockHostSet* host_set_2 = cluster_2.priority_set_.getMockHostSet(0); + auto host_2 = std::make_shared>(); + host_set_2->hosts_.emplace_back(host_2); + const std::string hostname_2 = "boo.com"; + + addHostInfo(*host_2, hostname_2, "tcp://1.2.3.5:8", locality, hostname_for_healthcheck, + "tcp://1.2.3.4:1", 3, 4); + + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + EXPECT_EQ(Http::Code::OK, + getCallback("/config_dump?include_eds&resource=dynamic_endpoint_configs", header_map, + response)); + std::string output = response.toString(); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump.DynamicEndpointConfig", + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "cluster_name": "fake_cluster", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "1.2.3.4", + "port_value": 80 + } + }, + "health_check_config": { + "port_value": 90, + "hostname": "test_hostname_healthcheck" + }, + "hostname": "foo.com" + }, + "health_status": "HEALTHY", + "metadata": {}, + "load_balancing_weight": 5 + } + ], + "priority": 6 + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + } + ] +} +)EOF"; + EXPECT_EQ(expected_json, output); +} + +// Test that using the mask query parameter filters the config dump. +// We add both static and dynamic listener config to the dump, but expect only +// dynamic in the JSON with ?mask=dynamic_listeners. +TEST_P(AdminInstanceTest, ConfigDumpFiltersByMask) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto listeners = admin_.getConfigTracker().add("listeners", [] { + auto msg = std::make_unique(); + auto dyn_listener = msg->add_dynamic_listeners(); + dyn_listener->set_name("foo"); + auto stat_listener = msg->add_static_listeners(); + envoy::config::listener::v3::Listener listener; + listener.set_name("bar"); + stat_listener->mutable_listener()->PackFrom(listener); + return msg; + }); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump", + "dynamic_listeners": [ + { + "name": "foo" + } + ] + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, + getCallback("/config_dump?mask=dynamic_listeners", header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + +ProtobufTypes::MessagePtr testDumpClustersConfig() { + auto msg = std::make_unique(); + auto* static_cluster = msg->add_static_clusters(); + envoy::config::cluster::v3::Cluster inner_cluster; + inner_cluster.set_name("foo"); + inner_cluster.set_ignore_health_on_host_removal(true); + static_cluster->mutable_cluster()->PackFrom(inner_cluster); + + auto* dyn_cluster = msg->add_dynamic_active_clusters(); + dyn_cluster->set_version_info("baz"); + dyn_cluster->mutable_last_updated()->set_seconds(5); + envoy::config::cluster::v3::Cluster inner_dyn_cluster; + inner_dyn_cluster.set_name("bar"); + inner_dyn_cluster.set_ignore_health_on_host_removal(true); + inner_dyn_cluster.mutable_http2_protocol_options()->set_allow_connect(true); + dyn_cluster->mutable_cluster()->PackFrom(inner_dyn_cluster); + return msg; +} + +// Test that when using both resource and mask query parameters the JSON output contains +// only the desired resource and the fields specified in the mask. +TEST_P(AdminInstanceTest, ConfigDumpFiltersByResourceAndMask) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto clusters = admin_.getConfigTracker().add("clusters", testDumpClustersConfig); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster", + "version_info": "baz", + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "bar", + "http2_protocol_options": { + "allow_connect": true + } + } + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?resource=dynamic_active_clusters&mask=" + "cluster.name,version_info,cluster.http2_protocol_options", + header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + +// Test that no fields are present in the JSON output if there is no intersection between the fields +// of the config dump and the fields present in the mask query parameter. +TEST_P(AdminInstanceTest, ConfigDumpNonExistentMask) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto clusters = admin_.getConfigTracker().add("clusters", testDumpClustersConfig); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump.StaticCluster" + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, + getCallback("/config_dump?resource=static_clusters&mask=bad", header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + +// Test that a 404 Not found is returned if a non-existent resource is passed in as the +// resource query parameter. +TEST_P(AdminInstanceTest, ConfigDumpNonExistentResource) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto listeners = admin_.getConfigTracker().add("listeners", [] { + auto msg = std::make_unique(); + msg->set_value("listeners_config"); + return msg; + }); + EXPECT_EQ(Http::Code::NotFound, getCallback("/config_dump?resource=foo", header_map, response)); +} + +// Test that a 400 Bad Request is returned if the passed resource query parameter is not a +// repeated field. +TEST_P(AdminInstanceTest, ConfigDumpResourceNotRepeated) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto clusters = admin_.getConfigTracker().add("clusters", [] { + auto msg = std::make_unique(); + msg->set_version_info("foo"); + return msg; + }); + EXPECT_EQ(Http::Code::BadRequest, + getCallback("/config_dump?resource=version_info", header_map, response)); +} + +} // namespace Server +} // namespace Envoy