From 7009f49068a035078433639491adca2c47f84d33 Mon Sep 17 00:00:00 2001 From: Yutong Li Date: Thu, 25 Jun 2020 11:51:48 -0700 Subject: [PATCH] server: add handler for dumping out eds (#11577) /config_dump API now supports dumping out EDS while using parameter ?include_eds Add help method dumpEndpointConfigs() to dump out EDS in /config_dump by calling this method in the handler handlerConfigDump() This will dump out envoy::admin::v3::EndpointsConfigDump by generating envoy::config::endpoint::v3::ClusterLoadAssignment based on data stored in server_.clusterManager().clusters() Missing Field: - ClusterLoadAssignment - Policy - endpoint_stale_after - StaticEndpointConfig - last_updated - DynamicEndpointConfig - version_info - last_updated Risk Level: Medium Testing: add unit test, integration test Docs Changes: operations_admin_interface Release Notes: N/A Part of fixing #3362 Signed-off-by: Yutong Li --- api/envoy/admin/v3/config_dump.proto | 13 +- api/envoy/admin/v4alpha/config_dump.proto | 13 +- docs/root/operations/admin.rst | 7 + docs/root/version_history/current.rst | 1 + .../envoy/admin/v3/config_dump.proto | 13 +- .../envoy/admin/v4alpha/config_dump.proto | 13 +- source/server/admin/BUILD | 1 + source/server/admin/admin.cc | 128 +++++- source/server/admin/admin.h | 11 +- test/integration/integration_admin_test.cc | 23 ++ test/server/admin/admin_test.cc | 369 ++++++++++++++++++ 11 files changed, 559 insertions(+), 33 deletions(-) diff --git a/api/envoy/admin/v3/config_dump.proto b/api/envoy/admin/v3/config_dump.proto index 0f51c56e6b37..73156697fdb2 100644 --- a/api/envoy/admin/v3/config_dump.proto +++ b/api/envoy/admin/v3/config_dump.proto @@ -30,9 +30,11 @@ message ConfigDump { // // * *bootstrap*: :ref:`BootstrapConfigDump ` // * *clusters*: :ref:`ClustersConfigDump ` + // * *endpoints*: :ref:`EndpointsConfigDump ` // * *listeners*: :ref:`ListenersConfigDump ` // * *routes*: :ref:`RoutesConfigDump ` - // [#not-implemented-hide:] * *endpoints*: :ref:`EndpointsConfigDump ` + // + // EDS Configuration will only be dumped by using parameter `?include_eds` // // You can filter output with the resource and mask query parameters. // See :ref:`/config_dump?resource={} `, @@ -348,8 +350,7 @@ message SecretsConfigDump { repeated DynamicSecret dynamic_warming_secrets = 3; } -// [#not-implemented-hide:] -// Envoy's EDS implementation *will* fill this message with all currently known endpoints. Endpoint +// Envoy's admin fill this message with all currently known endpoints. Endpoint // configuration information can be used to recreate an Envoy configuration by populating all // endpoints as static endpoints or by returning them in an EDS response. message EndpointsConfigDump { @@ -357,12 +358,12 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 1; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 2; } message DynamicEndpointConfig { - // This is the per-resource version information. This version is currently taken from the + // [#not-implemented-hide:] This is the per-resource version information. This version is currently taken from the // :ref:`version_info ` field at the time that // the endpoint configuration was loaded. string version_info = 1; @@ -370,7 +371,7 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 2; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; } diff --git a/api/envoy/admin/v4alpha/config_dump.proto b/api/envoy/admin/v4alpha/config_dump.proto index ca1399b21deb..8bbd5743219d 100644 --- a/api/envoy/admin/v4alpha/config_dump.proto +++ b/api/envoy/admin/v4alpha/config_dump.proto @@ -30,9 +30,11 @@ message ConfigDump { // // * *bootstrap*: :ref:`BootstrapConfigDump ` // * *clusters*: :ref:`ClustersConfigDump ` + // * *endpoints*: :ref:`EndpointsConfigDump ` // * *listeners*: :ref:`ListenersConfigDump ` // * *routes*: :ref:`RoutesConfigDump ` - // [#not-implemented-hide:] * *endpoints*: :ref:`EndpointsConfigDump ` + // + // EDS Configuration will only be dumped by using parameter `?include_eds` // // You can filter output with the resource and mask query parameters. // See :ref:`/config_dump?resource={} `, @@ -342,8 +344,7 @@ message SecretsConfigDump { repeated DynamicSecret dynamic_warming_secrets = 3; } -// [#not-implemented-hide:] -// Envoy's EDS implementation *will* fill this message with all currently known endpoints. Endpoint +// Envoy's admin fill this message with all currently known endpoints. Endpoint // configuration information can be used to recreate an Envoy configuration by populating all // endpoints as static endpoints or by returning them in an EDS response. message EndpointsConfigDump { @@ -356,7 +357,7 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 1; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 2; } @@ -364,7 +365,7 @@ message EndpointsConfigDump { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.EndpointsConfigDump.DynamicEndpointConfig"; - // This is the per-resource version information. This version is currently taken from the + // [#not-implemented-hide:] This is the per-resource version information. This version is currently taken from the // :ref:`version_info ` field at the time that // the endpoint configuration was loaded. string version_info = 1; @@ -372,7 +373,7 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 2; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; } diff --git a/docs/root/operations/admin.rst b/docs/root/operations/admin.rst index b90a1461f415..b2ad2e7c6391 100644 --- a/docs/root/operations/admin.rst +++ b/docs/root/operations/admin.rst @@ -137,6 +137,13 @@ modify different aspects of the server: The underlying proto is marked v2alpha and hence its contents, including the JSON representation, are not guaranteed to be stable. +.. _operations_admin_interface_config_dump_include_eds: + +.. http:get:: /config_dump?include_eds + + Dump currently loaded configuration including EDS. See the :ref:`response definition ` for more + information. + .. _operations_admin_interface_config_dump_by_mask: .. http:get:: /config_dump?mask={} diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 8d133b922f81..95bd4c5bf88b 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -59,6 +59,7 @@ New Features * access loggers: extened specifier for FilterStateFormatter to output :ref:`unstructured log string `. * access loggers: file access logger config added :ref:`log_format `. * access loggers: gRPC access logger config added added :ref:`API version ` to explicitly set the version of gRPC service endpoint and message to be used. +* admin: added support for dumping EDS config at :ref:`/config_dump?include_eds `. * aggregate cluster: make route :ref:`retry_priority ` predicates work with :ref:`this cluster type `. * build: official released binary is now built on Ubuntu 18.04, requires glibc >= 2.27. * build: official released binary is now built with Clang 10.0.0. diff --git a/generated_api_shadow/envoy/admin/v3/config_dump.proto b/generated_api_shadow/envoy/admin/v3/config_dump.proto index 0f51c56e6b37..73156697fdb2 100644 --- a/generated_api_shadow/envoy/admin/v3/config_dump.proto +++ b/generated_api_shadow/envoy/admin/v3/config_dump.proto @@ -30,9 +30,11 @@ message ConfigDump { // // * *bootstrap*: :ref:`BootstrapConfigDump ` // * *clusters*: :ref:`ClustersConfigDump ` + // * *endpoints*: :ref:`EndpointsConfigDump ` // * *listeners*: :ref:`ListenersConfigDump ` // * *routes*: :ref:`RoutesConfigDump ` - // [#not-implemented-hide:] * *endpoints*: :ref:`EndpointsConfigDump ` + // + // EDS Configuration will only be dumped by using parameter `?include_eds` // // You can filter output with the resource and mask query parameters. // See :ref:`/config_dump?resource={} `, @@ -348,8 +350,7 @@ message SecretsConfigDump { repeated DynamicSecret dynamic_warming_secrets = 3; } -// [#not-implemented-hide:] -// Envoy's EDS implementation *will* fill this message with all currently known endpoints. Endpoint +// Envoy's admin fill this message with all currently known endpoints. Endpoint // configuration information can be used to recreate an Envoy configuration by populating all // endpoints as static endpoints or by returning them in an EDS response. message EndpointsConfigDump { @@ -357,12 +358,12 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 1; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 2; } message DynamicEndpointConfig { - // This is the per-resource version information. This version is currently taken from the + // [#not-implemented-hide:] This is the per-resource version information. This version is currently taken from the // :ref:`version_info ` field at the time that // the endpoint configuration was loaded. string version_info = 1; @@ -370,7 +371,7 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 2; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; } diff --git a/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto b/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto index ca1399b21deb..8bbd5743219d 100644 --- a/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto +++ b/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto @@ -30,9 +30,11 @@ message ConfigDump { // // * *bootstrap*: :ref:`BootstrapConfigDump ` // * *clusters*: :ref:`ClustersConfigDump ` + // * *endpoints*: :ref:`EndpointsConfigDump ` // * *listeners*: :ref:`ListenersConfigDump ` // * *routes*: :ref:`RoutesConfigDump ` - // [#not-implemented-hide:] * *endpoints*: :ref:`EndpointsConfigDump ` + // + // EDS Configuration will only be dumped by using parameter `?include_eds` // // You can filter output with the resource and mask query parameters. // See :ref:`/config_dump?resource={} `, @@ -342,8 +344,7 @@ message SecretsConfigDump { repeated DynamicSecret dynamic_warming_secrets = 3; } -// [#not-implemented-hide:] -// Envoy's EDS implementation *will* fill this message with all currently known endpoints. Endpoint +// Envoy's admin fill this message with all currently known endpoints. Endpoint // configuration information can be used to recreate an Envoy configuration by populating all // endpoints as static endpoints or by returning them in an EDS response. message EndpointsConfigDump { @@ -356,7 +357,7 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 1; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 2; } @@ -364,7 +365,7 @@ message EndpointsConfigDump { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.EndpointsConfigDump.DynamicEndpointConfig"; - // This is the per-resource version information. This version is currently taken from the + // [#not-implemented-hide:] This is the per-resource version information. This version is currently taken from the // :ref:`version_info ` field at the time that // the endpoint configuration was loaded. string version_info = 1; @@ -372,7 +373,7 @@ message EndpointsConfigDump { // The endpoint config. google.protobuf.Any endpoint_config = 2; - // The timestamp when the Endpoint was last updated. + // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; } diff --git a/source/server/admin/BUILD b/source/server/admin/BUILD index 1ddc534ef0bc..2fb1cd56b1ac 100644 --- a/source/server/admin/BUILD +++ b/source/server/admin/BUILD @@ -67,6 +67,7 @@ envoy_cc_library( "//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", ], diff --git a/source/server/admin/admin.cc b/source/server/admin/admin.cc index 208d1caf7595..20b08bc3100a 100644 --- a/source/server/admin/admin.cc +++ b/source/server/admin/admin.cc @@ -14,11 +14,13 @@ #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" #include "envoy/server/options.h" #include "envoy/upstream/cluster_manager.h" +#include "envoy/upstream/outlier_detection.h" #include "envoy/upstream/upstream.h" #include "common/access_log/access_log_impl.h" @@ -130,6 +132,11 @@ 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; +} + // Helper method that ensures that we've setting flags based on all the health flag values on the // host. void setHealthFlag(Upstream::Host::HealthFlag flag, const Upstream::Host& host, @@ -449,8 +456,16 @@ Http::Code AdminImpl::handlerClusters(absl::string_view url, } void AdminImpl::addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, - const absl::optional& mask) const { - for (const auto& key_callback_pair : config_tracker_.getCallbacksMap()) { + 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& key_callback_pair : callbacks_map) { ProtobufTypes::MessagePtr message = key_callback_pair.second(); ASSERT(message); @@ -469,9 +484,16 @@ void AdminImpl::addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, absl::optional> AdminImpl::addResourceToDump(envoy::admin::v3::ConfigDump& dump, - const absl::optional& mask, - const std::string& resource) const { - for (const auto& key_callback_pair : config_tracker_.getCallbacksMap()) { + 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& key_callback_pair : callbacks_map) { ProtobufTypes::MessagePtr message = key_callback_pair.second(); ASSERT(message); @@ -506,23 +528,115 @@ AdminImpl::addResourceToDump(envoy::admin::v3::ConfigDump& dump, 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 (auto& cluster_pair : server_.clusterManager().clusters()) { + const Upstream::Cluster& cluster = cluster_pair.second.get(); + Upstream::ClusterInfoConstSharedPtr cluster_info = cluster.info(); + envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; + + if (cluster_info->eds_service_name().has_value()) { + cluster_load_assignment.set_cluster_name(cluster_info->eds_service_name().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::parseQueryString(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()); + 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); + addAllConfigToDump(dump, mask, include_eds); } MessageUtil::redact(dump); diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index 439cacf0013e..51818a6a714e 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -259,7 +259,7 @@ class AdminImpl : public Admin, * Helper methods for the /config_dump url handler. */ void addAllConfigToDump(envoy::admin::v3::ConfigDump& dump, - const absl::optional& mask) const; + 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 @@ -267,10 +267,17 @@ class AdminImpl : public Admin, */ absl::optional> addResourceToDump(envoy::admin::v3::ConfigDump& dump, const absl::optional& mask, - const std::string& resource) const; + 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. */ diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index 17df926b2b3c..7dec5deabe8f 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -358,6 +358,29 @@ TEST_P(IntegrationAdminTest, Admin) { config_dump.configs(5).UnpackTo(&secret_config_dump); EXPECT_EQ("secret_static_0", secret_config_dump.static_secrets(0).name()); + EXPECT_EQ("200", request("admin", "GET", "/config_dump?include_eds", response)); + EXPECT_EQ("application/json", ContentType(response)); + json = Json::Factory::loadFromString(response->body()); + index = 0; + const std::string expected_types_eds[] = { + "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump", + "type.googleapis.com/envoy.admin.v3.ClustersConfigDump", + "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", + "type.googleapis.com/envoy.admin.v3.ListenersConfigDump", + "type.googleapis.com/envoy.admin.v3.ScopedRoutesConfigDump", + "type.googleapis.com/envoy.admin.v3.RoutesConfigDump", + "type.googleapis.com/envoy.admin.v3.SecretsConfigDump"}; + + for (const Json::ObjectSharedPtr& obj_ptr : json->getObjectArray("configs")) { + EXPECT_TRUE(expected_types_eds[index].compare(obj_ptr->getString("@type")) == 0); + index++; + } + + // Validate we can parse as proto. + envoy::admin::v3::ConfigDump config_dump_with_eds; + TestUtility::loadFromJson(response->body(), config_dump_with_eds); + EXPECT_EQ(7, config_dump_with_eds.configs_size()); + // Validate that the "inboundonly" does not stop the default listener. response = IntegrationUtil::makeSingleRequest(lookupPort("admin"), "POST", "/drain_listeners?inboundonly", "", diff --git a/test/server/admin/admin_test.cc b/test/server/admin/admin_test.cc index 8b80d8d9ba71..443ae80595af 100644 --- a/test/server/admin/admin_test.cc +++ b/test/server/admin/admin_test.cc @@ -1,7 +1,9 @@ #include #include +#include #include #include +#include #include "envoy/admin/v3/clusters.pb.h" #include "envoy/admin/v3/config_dump.pb.h" @@ -10,11 +12,14 @@ #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" #include "common/http/message_impl.h" #include "common/json/json_loader.h" #include "common/protobuf/protobuf.h" #include "common/protobuf/utility.h" +#include "common/upstream/upstream_impl.h" #include "test/server/admin/admin_instance.h" #include "test/test_common/logging.h" @@ -217,6 +222,283 @@ TEST_P(AdminInstanceTest, ConfigDumpMaintainsOrder) { } } +// 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. @@ -248,6 +530,93 @@ TEST_P(AdminInstanceTest, ConfigDumpFiltersByResource) { 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.