diff --git a/api/envoy/admin/v3/config_dump.proto b/api/envoy/admin/v3/config_dump.proto index 9ec135a4212f1..8f5fa096e3f5d 100644 --- a/api/envoy/admin/v3/config_dump.proto +++ b/api/envoy/admin/v3/config_dump.proto @@ -32,6 +32,8 @@ message ConfigDump { // // * ``bootstrap``: :ref:`BootstrapConfigDump ` // * ``clusters``: :ref:`ClustersConfigDump ` + // * ``ecds_filter_http``: :ref:`EcdsConfigDump ` + // * ``ecds_filter_tcp_listener``: :ref:`EcdsConfigDump ` // * ``endpoints``: :ref:`EndpointsConfigDump ` // * ``listeners``: :ref:`ListenersConfigDump ` // * ``scoped_routes``: :ref:`ScopedRoutesConfigDump ` @@ -40,6 +42,9 @@ message ConfigDump { // // EDS Configuration will only be dumped by using parameter ``?include_eds`` // + // Currently ECDS is supported in HTTP and listener filters. Note, ECDS configuration for + // either HTTP or listener filter will only be dumped if it is actually configured. + // // You can filter output with the resource and mask query parameters. // See :ref:`/config_dump?resource={} `, // :ref:`/config_dump?mask={} `, diff --git a/api/envoy/admin/v3/config_dump_shared.proto b/api/envoy/admin/v3/config_dump_shared.proto index 6677dac586d87..8de77e18e1f83 100644 --- a/api/envoy/admin/v3/config_dump_shared.proto +++ b/api/envoy/admin/v3/config_dump_shared.proto @@ -370,3 +370,43 @@ message EndpointsConfigDump { // The dynamically loaded endpoint configs. repeated DynamicEndpointConfig dynamic_endpoint_configs = 3; } + +// Envoy's ECDS service fills this message with all currently extension +// configuration. Extension configuration information can be used to recreate +// an Envoy ECDS listener and HTTP filters as static filters or by returning +// them in ECDS response. +message EcdsConfigDump { + option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v2alpha.EcdsConfigDump"; + + // [#next-free-field: 6] + message EcdsFilterConfig { + option (udpa.annotations.versioning).previous_message_type = + "envoy.admin.v2alpha.EcdsConfigDump.EcdsFilterConfig"; + + // This is the per-resource version information. This version is currently + // taken from the :ref:`version_info + // ` + // field at the time that the ECDS filter was loaded. + string version_info = 1; + + // The ECDS filter config. + google.protobuf.Any ecds_filter = 2; + + // The timestamp when the ECDS filter was last updated. + google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The ``error_state`` field contains the rejected version of this + // particular resource along with the reason and timestamp. For successfully + // updated or acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; + + // The client status of this resource. + // [#not-implemented-hide:] + ClientResourceStatus client_status = 5; + } + + // The ECDS filter configs. + repeated EcdsFilterConfig ecds_filters = 1; +} diff --git a/docs/root/operations/admin.rst b/docs/root/operations/admin.rst index adf29ae92b14d..205ac374741ba 100644 --- a/docs/root/operations/admin.rst +++ b/docs/root/operations/admin.rst @@ -215,6 +215,10 @@ modify different aspects of the server: - :ref:`envoy.extensions.transport_sockets.tls.v3.Secret ` - :ref:`envoy.config.endpoint.v3.ClusterLoadAssignment ` + For ECDS config dump, the matched name field is the corresponding filter name, which is stored in: + + - :ref:`envoy.config.core.v3.TypedExtensionConfig.name ` + .. _operations_admin_interface_config_dump_by_resource_and_mask: .. http:get:: /config_dump?resource={}&mask={} diff --git a/source/common/filter/BUILD b/source/common/filter/BUILD index 2a2007f99aa65..e3180f7e42386 100644 --- a/source/common/filter/BUILD +++ b/source/common/filter/BUILD @@ -26,6 +26,7 @@ envoy_cc_library( "//source/common/init:target_lib", "//source/common/init:watcher_lib", "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/admin/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) diff --git a/source/common/filter/config_discovery_impl.cc b/source/common/filter/config_discovery_impl.cc index c10d265b3c24d..227b37949bbc0 100644 --- a/source/common/filter/config_discovery_impl.cc +++ b/source/common/filter/config_discovery_impl.cc @@ -131,6 +131,7 @@ void FilterConfigSubscription::onConfigUpdate( last_type_url_ = type_url; last_version_info_ = version_info; last_factory_name_ = factory_name; + last_updated_ = factory_context_.timeSource().systemTime(); } void FilterConfigSubscription::onConfigUpdate( @@ -151,6 +152,7 @@ void FilterConfigSubscription::onConfigUpdate( last_type_url_ = ""; last_version_info_ = ""; last_factory_name_ = ""; + last_updated_ = factory_context_.timeSource().systemTime(); } else if (!added_resources.empty()) { onConfigUpdate(added_resources, added_resources[0].get().version()); } @@ -177,6 +179,9 @@ void FilterConfigSubscription::incrementConflictCounter() { stats_.config_confli std::shared_ptr FilterConfigProviderManagerImplBase::getSubscription( const envoy::config::core::v3::ConfigSource& config_source, const std::string& name, Server::Configuration::ServerFactoryContext& server_context, const std::string& stat_prefix) { + // There are ECDS filters configured. Setup ECDS config dump call backs. + setupEcdsConfigDumpCallbacks(server_context.admin()); + // FilterConfigSubscriptions are unique based on their config source and filter config name // combination. // TODO(https://github.com/envoyproxy/envoy/issues/11967) Hash collision can cause subscription diff --git a/source/common/filter/config_discovery_impl.h b/source/common/filter/config_discovery_impl.h index a6678458c18f5..e5f06102cd7f9 100644 --- a/source/common/filter/config_discovery_impl.h +++ b/source/common/filter/config_discovery_impl.h @@ -1,5 +1,6 @@ #pragma once +#include "envoy/admin/v3/config_dump.pb.h" #include "envoy/config/core/v3/extension.pb.h" #include "envoy/config/core/v3/extension.pb.validate.h" #include "envoy/config/extension_config_provider.h" @@ -7,6 +8,7 @@ #include "envoy/filter/config_provider_manager.h" #include "envoy/http/filter.h" #include "envoy/protobuf/message_validator.h" +#include "envoy/server/admin.h" #include "envoy/server/factory_context.h" #include "envoy/singleton/instance.h" #include "envoy/stats/scope.h" @@ -307,6 +309,8 @@ class FilterConfigSubscription const std::string& lastTypeUrl() { return last_type_url_; } const std::string& lastVersionInfo() { return last_version_info_; } const std::string& lastFactoryName() { return last_factory_name_; } + const SystemTime& lastUpdated() { return last_updated_; } + void incrementConflictCounter(); private: @@ -327,6 +331,7 @@ class FilterConfigSubscription std::string last_type_url_; std::string last_version_info_; std::string last_factory_name_; + SystemTime last_updated_; Server::Configuration::ServerFactoryContext& factory_context_; Init::SharedTargetImpl init_target_; @@ -387,9 +392,44 @@ class FilterConfigProviderManagerImplBase : Logger::Loggable absl::string_view type_url) const; void validateProtoConfigTypeUrl(const std::string& type_url, const absl::flat_hash_set& require_type_urls) const; + // Return the config dump map key string for the corresponding ECDS filter type. + virtual const std::string getConfigDumpType() const PURE; private: + void setupEcdsConfigDumpCallbacks(OptRef admin) { + if (admin.has_value()) { + if (config_tracker_entry_ == nullptr) { + config_tracker_entry_ = admin->getConfigTracker().add( + getConfigDumpType(), [this](const Matchers::StringMatcher& name_matcher) { + return dumpEcdsFilterConfigs(name_matcher); + }); + } + } + } + + ProtobufTypes::MessagePtr dumpEcdsFilterConfigs(const Matchers::StringMatcher& name_matcher) { + auto config_dump = std::make_unique(); + for (const auto& subscription : subscriptions_) { + const auto& ecds_filter = subscription.second.lock(); + if (!ecds_filter || !name_matcher.match(ecds_filter->name())) { + continue; + } + envoy::config::core::v3::TypedExtensionConfig filter_config; + filter_config.set_name(ecds_filter->name()); + if (ecds_filter->lastConfig()) { + filter_config.mutable_typed_config()->PackFrom(*ecds_filter->lastConfig()); + } + auto& filter_config_dump = *config_dump->mutable_ecds_filters()->Add(); + filter_config_dump.mutable_ecds_filter()->PackFrom(filter_config); + filter_config_dump.set_version_info(ecds_filter->lastVersionInfo()); + TimestampUtil::systemClockToTimestamp(ecds_filter->lastUpdated(), + *(filter_config_dump.mutable_last_updated())); + } + return config_dump; + } + absl::flat_hash_map> subscriptions_; + Server::ConfigTracker::EntryOwnerPtr config_tracker_entry_; friend class FilterConfigSubscription; }; @@ -528,6 +568,7 @@ class HttpFilterConfigProviderManagerImpl Config::Utility::validateTerminalFilters(filter_config_name, filter_type, filter_chain_type, is_terminal_filter, last_filter_in_filter_chain); } + const std::string getConfigDumpType() const override { return "ecds_filter_http"; } }; // HTTP filter @@ -554,6 +595,7 @@ class UpstreamHttpFilterConfigProviderManagerImpl Config::Utility::validateTerminalFilters(filter_config_name, filter_type, filter_chain_type, is_terminal_filter, last_filter_in_filter_chain); } + const std::string getConfigDumpType() const override { return "ecds_filter_upstream_http"; } }; // TCP listener filter @@ -564,6 +606,9 @@ class TcpListenerFilterConfigProviderManagerImpl TcpListenerDynamicFilterConfigProviderImpl> { public: absl::string_view statPrefix() const override { return "tcp_listener_filter."; } + +protected: + const std::string getConfigDumpType() const override { return "ecds_filter_tcp_listener"; } }; // UDP listener filter @@ -574,6 +619,9 @@ class UdpListenerFilterConfigProviderManagerImpl UdpListenerDynamicFilterConfigProviderImpl> { public: absl::string_view statPrefix() const override { return "udp_listener_filter."; } + +protected: + const std::string getConfigDumpType() const override { return "ecds_filter_udp_listener"; } }; } // namespace Filter diff --git a/test/integration/BUILD b/test/integration/BUILD index c02abfb629bac..1b734c764a0cc 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -1318,6 +1318,7 @@ envoy_cc_test( "//test/integration/filters:set_is_terminal_filter_lib", "//test/integration/filters:set_response_code_filter_config_proto_cc_proto", "//test/integration/filters:set_response_code_filter_lib", + "//test/integration/filters:test_listener_filter_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/extensions/common/matching/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", diff --git a/test/integration/extension_discovery_integration_test.cc b/test/integration/extension_discovery_integration_test.cc index af503ca060ff0..04c6291142ca4 100644 --- a/test/integration/extension_discovery_integration_test.cc +++ b/test/integration/extension_discovery_integration_test.cc @@ -7,6 +7,7 @@ #include "test/config/v2_link_hacks.h" #include "test/integration/filters/set_is_terminal_filter_config.pb.h" #include "test/integration/filters/set_response_code_filter_config.pb.h" +#include "test/integration/filters/test_listener_filter.pb.h" #include "test/integration/http_integration.h" #include "test/test_common/utility.h" @@ -131,17 +132,41 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara }); } + void addEcdsCluster(const std::string& cluster_name) { + // Add an xDS cluster for extension config discovery. + config_helper_.addConfigModifier( + [cluster_name](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* ecds_cluster = bootstrap.mutable_static_resources()->add_clusters(); + ecds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + ecds_cluster->set_name(cluster_name); + ConfigHelper::setHttp2(*ecds_cluster); + }); + } + + void addDynamicListenerFilter(const std::string& name) { + config_helper_.addConfigModifier( + [name, this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + auto* listener_filter = listener->add_listener_filters(); + listener_filter->set_name(name); + auto* discovery = listener_filter->mutable_config_discovery(); + discovery->add_type_urls( + "type.googleapis.com/test.integration.filters.TestTcpListenerFilterConfig"); + discovery->mutable_config_source()->set_resource_api_version( + envoy::config::core::v3::ApiVersion::V3); + auto* api_config_source = discovery->mutable_config_source()->mutable_api_config_source(); + api_config_source->set_api_type(envoy::config::core::v3::ApiConfigSource::GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::ApiVersion::V3); + auto* grpc_service = api_config_source->add_grpc_services(); + setGrpcService(*grpc_service, "ecds2_cluster", getEcds2FakeUpstream().localAddress()); + }); + } + void initialize() override { defer_listener_finalization_ = true; setUpstreamCount(1); - // Add an xDS cluster for extension config discovery. - config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - auto* ecds_cluster = bootstrap.mutable_static_resources()->add_clusters(); - ecds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - ecds_cluster->set_name("ecds_cluster"); - ConfigHelper::setHttp2(*ecds_cluster); - }); + addEcdsCluster("ecds_cluster"); // Make HCM do a direct response to avoid timing issues with the upstream. config_helper_.addConfigModifier( [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& @@ -160,6 +185,12 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara lds_cluster->set_name("lds_cluster"); ConfigHelper::setHttp2(*lds_cluster); }); + + // In case to configure both HTTP and Listener ECDS filters, adding the 2nd ECDS cluster. + if (two_ecds_filters_) { + addEcdsCluster("ecds2_cluster"); + } + // Must be the last since it nukes static listeners. config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { listener_config_.Swap(bootstrap.mutable_static_resources()->mutable_listeners(0)); @@ -201,6 +232,19 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara addFakeUpstream(Http::CodecType::HTTP2); // Create the listener config discovery upstream (fake_upstreams_[2]). addFakeUpstream(Http::CodecType::HTTP2); + if (two_ecds_filters_) { + addFakeUpstream(Http::CodecType::HTTP2); + } + } + + // Wait for ECDS stream. + void waitForEcdsStream(FakeUpstream& upstream, FakeHttpConnectionPtr& connection, + FakeStreamPtr& stream) { + AssertionResult result = upstream.waitForHttpConnection(*dispatcher_, connection); + ASSERT_TRUE(result); + result = connection->waitForNewStream(*dispatcher_, stream); + ASSERT_TRUE(result); + stream->startGrpcStream(); } void waitXdsStream() { @@ -215,13 +259,11 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara // Response with initial LDS. sendLdsResponse("initial"); - // Wait for ECDS stream. - auto& ecds_upstream = getEcdsFakeUpstream(); - result = ecds_upstream.waitForHttpConnection(*dispatcher_, ecds_connection_); - RELEASE_ASSERT(result, result.message()); - result = ecds_connection_->waitForNewStream(*dispatcher_, ecds_stream_); - RELEASE_ASSERT(result, result.message()); - ecds_stream_->startGrpcStream(); + waitForEcdsStream(getEcdsFakeUpstream(), ecds_connection_, ecds_stream_); + if (two_ecds_filters_) { + // Wait for 2nd ECDS stream. + waitForEcdsStream(getEcds2FakeUpstream(), ecds2_connection_, ecds2_stream_); + } } void sendLdsResponse(const std::string& version) { @@ -232,19 +274,28 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara lds_stream_->sendGrpcMessage(response); } - void sendXdsResponse(const std::string& name, const std::string& version, - const std::string& yaml_config, bool ttl = false, - bool is_set_resp_code_config = true) { + void sendEcdsResponse(const envoy::config::core::v3::TypedExtensionConfig& typed_config, + const std::string& name, const std::string& version, const bool ttl, + FakeStreamPtr& ecds_stream) { + envoy::service::discovery::v3::Resource resource; + resource.set_name(name); + if (ttl) { + resource.mutable_ttl()->set_seconds(1); + } + resource.mutable_resource()->PackFrom(typed_config); + envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); response.set_type_url("type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig"); + response.add_resources()->PackFrom(resource); + ecds_stream->sendGrpcMessage(response); + } + void sendHttpFilterEcdsResponse(const std::string& name, const std::string& version, + const std::string& yaml_config, bool ttl = false, + bool is_set_resp_code_config = true) { envoy::config::core::v3::TypedExtensionConfig typed_config; typed_config.set_name(name); - - envoy::service::discovery::v3::Resource resource; - resource.set_name(name); - if (is_set_resp_code_config) { const auto configuration = TestUtility::parseYaml( @@ -256,29 +307,48 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara yaml_config); typed_config.mutable_typed_config()->PackFrom(configuration); } - resource.mutable_resource()->PackFrom(typed_config); - if (ttl) { - resource.mutable_ttl()->set_seconds(1); - } - response.add_resources()->PackFrom(resource); - ecds_stream_->sendGrpcMessage(response); + sendEcdsResponse(typed_config, name, version, ttl, ecds_stream_); } - void sendXdsResponseWithFullYaml(const std::string& name, const std::string& version, - const std::string& full_yaml) { - envoy::service::discovery::v3::DiscoveryResponse response; - response.set_version_info(version); - response.set_type_url("type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig"); + void sendListenerFilterEcdsResponse(const std::string& name, const std::string& version, + const uint32_t drain_bytes) { + envoy::config::core::v3::TypedExtensionConfig typed_config; + typed_config.set_name(name); + auto configuration = test::integration::filters::TestTcpListenerFilterConfig(); + configuration.set_drain_bytes(drain_bytes); + typed_config.mutable_typed_config()->PackFrom(configuration); + sendEcdsResponse(typed_config, name, version, false, ecds2_stream_); + } + + void sendHttpFilterEcdsResponseWithFullYaml(const std::string& name, const std::string& version, + const std::string& full_yaml) { const auto configuration = TestUtility::parseYaml(full_yaml); envoy::config::core::v3::TypedExtensionConfig typed_config; typed_config.set_name(name); typed_config.mutable_typed_config()->MergeFrom(configuration); - response.add_resources()->PackFrom(typed_config); - ecds_stream_->sendGrpcMessage(response); + sendEcdsResponse(typed_config, name, version, false, ecds_stream_); } + absl::string_view request(const std::string port_key, const std::string method, + const std::string endpoint, BufferingStreamDecoderPtr& response) { + response = IntegrationUtil::makeSingleRequest(lookupPort(port_key), method, endpoint, "", + Http::CodecType::HTTP1, version_); + EXPECT_TRUE(response->complete()); + return response->headers().getStatusValue(); + } + + absl::string_view contentType(const BufferingStreamDecoderPtr& response) { + const Http::HeaderEntry* entry = response->headers().ContentType(); + if (entry == nullptr) { + return "(null)"; + } + return entry->value().getStringView(); + } + + bool two_ecds_filters_{false}; FakeUpstream& getEcdsFakeUpstream() const { return *fake_upstreams_[1]; } FakeUpstream& getLdsFakeUpstream() const { return *fake_upstreams_[2]; } + FakeUpstream& getEcds2FakeUpstream() const { return *fake_upstreams_[3]; } // gRPC LDS set-up envoy::config::listener::v3::Listener listener_config_; @@ -289,6 +359,8 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara // gRPC ECDS set-up FakeHttpConnectionPtr ecds_connection_{nullptr}; FakeStreamPtr ecds_stream_{nullptr}; + FakeHttpConnectionPtr ecds2_connection_{nullptr}; + FakeStreamPtr ecds2_stream_{nullptr}; }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, ExtensionDiscoveryIntegrationTest, @@ -301,7 +373,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccess) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", denyPrivateConfig()); + sendHttpFilterEcdsResponse("foo", "1", denyPrivateConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -325,7 +397,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccess) { } // Update again but keep the connection. { - sendXdsResponse("foo", "2", allowAllConfig()); + sendHttpFilterEcdsResponse("foo", "2", allowAllConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 2); auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); ASSERT_TRUE(response->waitForEndStream()); @@ -341,7 +413,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccessWithTtl) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", denyPrivateConfig(), true); + sendHttpFilterEcdsResponse("foo", "1", denyPrivateConfig(), true); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -376,7 +448,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccessWithTtl) { { // Reinstate the previous configuration. - sendXdsResponse("foo", "1", denyPrivateConfig(), true); + sendHttpFilterEcdsResponse("foo", "1", denyPrivateConfig(), true); // Wait until the new configuration has been applied. test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 3); auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); @@ -393,7 +465,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccessWithTtlWithDefault) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", allowAllConfig(), true); + sendHttpFilterEcdsResponse("foo", "1", allowAllConfig(), true); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -427,7 +499,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccessWithMatcher) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponseWithFullYaml("foo", "1", denyPrivateConfigWithMatcher()); + sendHttpFilterEcdsResponseWithFullYaml("foo", "1", denyPrivateConfigWithMatcher()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -469,7 +541,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicDefaultMatcher) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", invalidConfig()); + sendHttpFilterEcdsResponse("foo", "1", invalidConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_fail", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -506,7 +578,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, ReuseExtensionConfig) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", allowAllConfig()); + sendHttpFilterEcdsResponse("foo", "1", allowAllConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -537,7 +609,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, ReuseExtensionConfigInvalid) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponseWithFullYaml("foo", "1", denyPrivateConfigWithMatcher()); + sendHttpFilterEcdsResponseWithFullYaml("foo", "1", denyPrivateConfigWithMatcher()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -574,7 +646,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailWithDefault) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", invalidConfig()); + sendHttpFilterEcdsResponse("foo", "1", invalidConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_fail", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -595,7 +667,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailWithoutDefault) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", invalidConfig()); + sendHttpFilterEcdsResponse("foo", "1", invalidConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_fail", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -630,7 +702,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicWithoutWarming) { } // Update should cause a different response. - sendXdsResponse("bar", "1", denyPrivateConfig()); + sendHttpFilterEcdsResponse("bar", "1", denyPrivateConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.bar.config_reload", 1); { auto response = codec_client_->makeHeaderOnlyRequest(request_headers); @@ -651,7 +723,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicWithoutWarmingFail) { test_server_->waitForGaugeGe("listener_manager.workers_started", 1); codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); // Update should not cause a different response. - sendXdsResponse("bar", "1", invalidConfig()); + sendHttpFilterEcdsResponse("bar", "1", invalidConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.bar.config_fail", 1); Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; @@ -669,7 +741,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicTwoSubscriptionsSameName) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("baz", "1", denyPrivateConfig()); + sendHttpFilterEcdsResponse("baz", "1", denyPrivateConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.baz.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -708,7 +780,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailTerminalFilterNotAtEndOfFilte test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", terminalFilterConfig(), false, false); + sendHttpFilterEcdsResponse("foo", "1", terminalFilterConfig(), false, false); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_fail", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -730,7 +802,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, ReloadBoth) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); registerTestServerPorts({"http"}); - sendXdsResponse("foo", "1", denyPrivateConfig()); + sendHttpFilterEcdsResponse("foo", "1", denyPrivateConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); test_server_->waitUntilListenersReady(); test_server_->waitForGaugeGe("listener_manager.workers_started", 1); @@ -765,7 +837,7 @@ TEST_P(ExtensionDiscoveryIntegrationTest, ReloadBoth) { // Update ECDS but keep the connection. { - sendXdsResponse("foo", "2", allowAllConfig()); + sendHttpFilterEcdsResponse("foo", "2", allowAllConfig()); test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 2); auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); ASSERT_TRUE(response->waitForEndStream()); @@ -775,5 +847,79 @@ TEST_P(ExtensionDiscoveryIntegrationTest, ReloadBoth) { codec_client_->close(); } +// ECDS config dump test with one listener ECDS filter and one HTTP ECDS filter. +TEST_P(ExtensionDiscoveryIntegrationTest, ConfigDumpWithTwoSubscriptionTypes) { + DISABLE_IF_ADMIN_DISABLED; // Uses admin interface. + two_ecds_filters_ = true; + on_server_init_function_ = [&]() { waitXdsStream(); }; + // HTTP ECDS filter + addDynamicFilter("foo", false); + // Listener ECDS filter + addDynamicListenerFilter("bar"); + initialize(); + + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + registerTestServerPorts({"http"}); + + // Send configuration update for HTTP ECDS filter. + sendHttpFilterEcdsResponse("foo", "1", denyPrivateConfig()); + // Send configuration update for listener ECDS filter. + sendListenerFilterEcdsResponse("bar", "2", 7); + test_server_->waitForCounterGe("extension_config_discovery.http_filter.foo.config_reload", 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + + // Get config_dump and verify HTTP and Listener ECDS filters are dumped correctly. + BufferingStreamDecoderPtr response; + EXPECT_EQ("200", request("admin", "GET", "/config_dump", response)); + EXPECT_EQ("application/json", contentType(response)); + Json::ObjectSharedPtr json = Json::Factory::loadFromString(response->body()); + size_t index = 0; + const std::string expected_types[] = { + "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump", + "type.googleapis.com/envoy.admin.v3.ClustersConfigDump", + "type.googleapis.com/envoy.admin.v3.EcdsConfigDump", // HTTP + "type.googleapis.com/envoy.admin.v3.EcdsConfigDump", // TCP Listener + "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[index].compare(obj_ptr->getString("@type")) == 0); + index++; + } + + // Validate we can parse as proto. + envoy::admin::v3::ConfigDump config_dump; + TestUtility::loadFromJson(response->body(), config_dump); + EXPECT_EQ(8, config_dump.configs_size()); + + // Unpack the HTTP filter. + envoy::admin::v3::EcdsConfigDump ecds_config_dump_http; + config_dump.configs(2).UnpackTo(&ecds_config_dump_http); + EXPECT_EQ("1", ecds_config_dump_http.ecds_filters(0).version_info()); + envoy::config::core::v3::TypedExtensionConfig http_filter_config; + EXPECT_TRUE(ecds_config_dump_http.ecds_filters(0).ecds_filter().UnpackTo(&http_filter_config)); + EXPECT_EQ("foo", http_filter_config.name()); + test::integration::filters::SetResponseCodeFilterConfig http_config; + http_filter_config.typed_config().UnpackTo(&http_config); + EXPECT_EQ("/private", http_config.prefix()); + EXPECT_EQ(403, http_config.code()); + + // Unpack the listener filter. + envoy::admin::v3::EcdsConfigDump ecds_config_dump_listener; + config_dump.configs(3).UnpackTo(&ecds_config_dump_listener); + EXPECT_EQ("2", ecds_config_dump_listener.ecds_filters(0).version_info()); + envoy::config::core::v3::TypedExtensionConfig filter_config; + EXPECT_TRUE(ecds_config_dump_listener.ecds_filters(0).ecds_filter().UnpackTo(&filter_config)); + EXPECT_EQ("bar", filter_config.name()); + test::integration::filters::TestTcpListenerFilterConfig listener_config; + filter_config.typed_config().UnpackTo(&listener_config); + EXPECT_EQ(7, listener_config.drain_bytes()); +} + } // namespace } // namespace Envoy diff --git a/test/integration/listener_extension_discovery_integration_test.cc b/test/integration/listener_extension_discovery_integration_test.cc index 36524805d127a..1c55b3a7b930c 100644 --- a/test/integration/listener_extension_discovery_integration_test.cc +++ b/test/integration/listener_extension_discovery_integration_test.cc @@ -17,6 +17,12 @@ enum class ListenerMatcherType { NULLMATCHER, ANYMATCHER, NOTANYMATCHER }; constexpr absl::string_view EcdsClusterName = "ecds_cluster"; constexpr absl::string_view Ecds2ClusterName = "ecds2_cluster"; +constexpr absl::string_view expected_types[] = { + "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump", + "type.googleapis.com/envoy.admin.v3.ClustersConfigDump", + "type.googleapis.com/envoy.admin.v3.EcdsConfigDump", + "type.googleapis.com/envoy.admin.v3.ListenersConfigDump", + "type.googleapis.com/envoy.admin.v3.SecretsConfigDump"}; class ListenerExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, public BaseIntegrationTest { @@ -222,6 +228,39 @@ class ListenerExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegra tcp_client->close(); } + // Verify ECDS config dump data. + bool + verifyConfigDumpData(envoy::config::core::v3::TypedExtensionConfig filter_config, + test::integration::filters::TestTcpListenerFilterConfig listener_config) { + // There is no ordering. i.e, either foo or bar could be the 1st in the config dump. + if (filter_config.name() == "foo") { + EXPECT_EQ(3, listener_config.drain_bytes()); + return true; + } else if (filter_config.name() == "bar") { + EXPECT_EQ(4, listener_config.drain_bytes()); + return true; + } else { + return false; + } + } + + // Utilities used for config dump. + absl::string_view request(const std::string port_key, const std::string method, + const std::string endpoint, BufferingStreamDecoderPtr& response) { + response = IntegrationUtil::makeSingleRequest(lookupPort(port_key), method, endpoint, "", + Http::CodecType::HTTP1, version_); + EXPECT_TRUE(response->complete()); + return response->headers().getStatusValue(); + } + + absl::string_view contentType(const BufferingStreamDecoderPtr& response) { + const Http::HeaderEntry* entry = response->headers().ContentType(); + if (entry == nullptr) { + return "(null)"; + } + return entry->value().getStringView(); + } + const uint32_t default_drain_bytes_{2}; const std::string filter_name_; const std::string data_; @@ -537,5 +576,158 @@ TEST_P(ListenerExtensionDiscoveryIntegrationTest, DestroyDuringInit) { ecds_connection_.reset(); } +// Basic ECDS config dump test with one filter. +TEST_P(ListenerExtensionDiscoveryIntegrationTest, BasicSuccessWithConfigDump) { + DISABLE_IF_ADMIN_DISABLED; // Uses admin interface. + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter(filter_name_, false); + initialize(); + + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + + // Send 1st config update to have listener filter drain 5 bytes of data. + sendXdsResponse(filter_name_, "1", 5); + test_server_->waitForCounterGe( + "extension_config_discovery.tcp_listener_filter." + filter_name_ + ".config_reload", 1); + + // Verify ECDS config dump are working correctly. + BufferingStreamDecoderPtr response; + EXPECT_EQ("200", request("admin", "GET", "/config_dump", response)); + EXPECT_EQ("application/json", contentType(response)); + Json::ObjectSharedPtr json = Json::Factory::loadFromString(response->body()); + size_t index = 0; + for (const Json::ObjectSharedPtr& obj_ptr : json->getObjectArray("configs")) { + EXPECT_TRUE(expected_types[index].compare(obj_ptr->getString("@type")) == 0); + index++; + } + + // Validate we can parse as proto. + envoy::admin::v3::ConfigDump config_dump; + TestUtility::loadFromJson(response->body(), config_dump); + EXPECT_EQ(5, config_dump.configs_size()); + + // With /config_dump, the response has the format: EcdsConfigDump. + envoy::admin::v3::EcdsConfigDump ecds_config_dump; + config_dump.configs(2).UnpackTo(&ecds_config_dump); + EXPECT_EQ("1", ecds_config_dump.ecds_filters(0).version_info()); + envoy::config::core::v3::TypedExtensionConfig filter_config; + EXPECT_TRUE(ecds_config_dump.ecds_filters(0).ecds_filter().UnpackTo(&filter_config)); + EXPECT_EQ("foo", filter_config.name()); + test::integration::filters::TestTcpListenerFilterConfig listener_config; + filter_config.typed_config().UnpackTo(&listener_config); + EXPECT_EQ(5, listener_config.drain_bytes()); +} + +// ECDS config dump test with the filter configuration being removed by TTL expired. +TEST_P(ListenerExtensionDiscoveryIntegrationTest, ConfigDumpWithFilterConfigRemovedByTtl) { + DISABLE_IF_ADMIN_DISABLED; // Uses admin interface. + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter(filter_name_, false, false); + initialize(); + + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + // Send config update with TTL 1s. + sendXdsResponse(filter_name_, "1", 5, true); + test_server_->waitForCounterGe( + "extension_config_discovery.tcp_listener_filter." + filter_name_ + ".config_reload", 1); + // Wait for configuration expired. + test_server_->waitForCounterGe( + "extension_config_discovery.tcp_listener_filter." + filter_name_ + ".config_reload", 2); + + BufferingStreamDecoderPtr response; + EXPECT_EQ("200", request("admin", "GET", "/config_dump?resource=ecds_filters", response)); + envoy::admin::v3::ConfigDump config_dump; + TestUtility::loadFromJson(response->body(), config_dump); + // With /config_dump?resource=ecds_filters, the response has the format: EcdsFilterConfig. + envoy::admin::v3::EcdsConfigDump::EcdsFilterConfig ecds_msg; + config_dump.configs(0).UnpackTo(&ecds_msg); + EXPECT_EQ("", ecds_msg.version_info()); + envoy::config::core::v3::TypedExtensionConfig filter_config; + EXPECT_TRUE(ecds_msg.ecds_filter().UnpackTo(&filter_config)); + EXPECT_EQ("foo", filter_config.name()); + // Verify ECDS config dump doesn't have the filter configuration. + EXPECT_EQ(false, filter_config.has_typed_config()); +} + +// ECDS config dump test with two filters. +TEST_P(ListenerExtensionDiscoveryIntegrationTest, TwoSubscriptionsSameFilterTypeWithConfigDump) { + DISABLE_IF_ADMIN_DISABLED; // Uses admin interface. + two_connections_ = true; + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", true); + addDynamicFilter("bar", false, true, false, ListenerMatcherType::NULLMATCHER, true); + initialize(); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + + sendXdsResponse("foo", "1", 3); + sendXdsResponse("bar", "1", 4, false, true); + test_server_->waitForCounterGe("extension_config_discovery.tcp_listener_filter.foo.config_reload", + 1); + test_server_->waitForCounterGe("extension_config_discovery.tcp_listener_filter.bar.config_reload", + 1); + // Verify ECDS config dump are working correctly. + BufferingStreamDecoderPtr response; + EXPECT_EQ("200", request("admin", "GET", "/config_dump", response)); + EXPECT_EQ("application/json", contentType(response)); + Json::ObjectSharedPtr json = Json::Factory::loadFromString(response->body()); + size_t index = 0; + for (const Json::ObjectSharedPtr& obj_ptr : json->getObjectArray("configs")) { + EXPECT_TRUE(expected_types[index].compare(obj_ptr->getString("@type")) == 0); + index++; + } + + envoy::admin::v3::ConfigDump config_dump; + TestUtility::loadFromJson(response->body(), config_dump); + EXPECT_EQ(5, config_dump.configs_size()); + envoy::admin::v3::EcdsConfigDump ecds_config_dump; + config_dump.configs(2).UnpackTo(&ecds_config_dump); + envoy::config::core::v3::TypedExtensionConfig filter_config; + test::integration::filters::TestTcpListenerFilterConfig listener_config; + // Verify the first filter. + EXPECT_EQ("1", ecds_config_dump.ecds_filters(0).version_info()); + EXPECT_TRUE(ecds_config_dump.ecds_filters(0).ecds_filter().UnpackTo(&filter_config)); + filter_config.typed_config().UnpackTo(&listener_config); + EXPECT_TRUE(verifyConfigDumpData(filter_config, listener_config)); + // Verify the second filter. + EXPECT_EQ("1", ecds_config_dump.ecds_filters(1).version_info()); + EXPECT_TRUE(ecds_config_dump.ecds_filters(1).ecds_filter().UnpackTo(&filter_config)); + filter_config.typed_config().UnpackTo(&listener_config); + EXPECT_TRUE(verifyConfigDumpData(filter_config, listener_config)); +} + +// ECDS config dump test with specified resource and regex name search. +TEST_P(ListenerExtensionDiscoveryIntegrationTest, TwoSubscriptionsConfigDumpWithResourceAndRegex) { + DISABLE_IF_ADMIN_DISABLED; // Uses admin interface. + two_connections_ = true; + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", true); + addDynamicFilter("bar", false, true, false, ListenerMatcherType::NULLMATCHER, true); + initialize(); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + + sendXdsResponse("foo", "1", 3); + sendXdsResponse("bar", "1", 4, false, true); + test_server_->waitForCounterGe("extension_config_discovery.tcp_listener_filter.foo.config_reload", + 1); + test_server_->waitForCounterGe("extension_config_discovery.tcp_listener_filter.bar.config_reload", + 1); + BufferingStreamDecoderPtr response; + EXPECT_EQ("200", + request("admin", "GET", "/config_dump?resource=ecds_filters&name_regex=.a.", response)); + + envoy::admin::v3::ConfigDump config_dump; + TestUtility::loadFromJson(response->body(), config_dump); + EXPECT_EQ(1, config_dump.configs_size()); + envoy::admin::v3::EcdsConfigDump::EcdsFilterConfig ecds_msg; + config_dump.configs(0).UnpackTo(&ecds_msg); + EXPECT_EQ("1", ecds_msg.version_info()); + envoy::config::core::v3::TypedExtensionConfig filter_config; + EXPECT_TRUE(ecds_msg.ecds_filter().UnpackTo(&filter_config)); + EXPECT_EQ("bar", filter_config.name()); + test::integration::filters::TestTcpListenerFilterConfig listener_config; + filter_config.typed_config().UnpackTo(&listener_config); + EXPECT_EQ(4, listener_config.drain_bytes()); +} + } // namespace } // namespace Envoy diff --git a/test/server/admin/BUILD b/test/server/admin/BUILD index 7eae434ab61c9..b6242f2f96cf1 100644 --- a/test/server/admin/BUILD +++ b/test/server/admin/BUILD @@ -211,6 +211,7 @@ envoy_cc_test( srcs = envoy_select_admin_functionality(["config_dump_handler_test.cc"]), deps = [ ":admin_instance_lib", + "//test/integration/filters:test_listener_filter_lib", ], ) diff --git a/test/server/admin/config_dump_handler_test.cc b/test/server/admin/config_dump_handler_test.cc index 13fd66efeb988..bc3c1b0ed5203 100644 --- a/test/server/admin/config_dump_handler_test.cc +++ b/test/server/admin/config_dump_handler_test.cc @@ -1,3 +1,4 @@ +#include "test/integration/filters/test_listener_filter.pb.h" #include "test/server/admin/admin_instance.h" using testing::HasSubstr; @@ -790,5 +791,72 @@ TEST_P(AdminInstanceTest, FieldMasksWorkWhenFetchingAllResources) { response.toString()); } +ProtobufTypes::MessagePtr testDumpEcdsConfig(const Matchers::StringMatcher&) { + auto msg = std::make_unique(); + auto* ecds = msg->mutable_ecds_filters()->Add(); + ecds->set_version_info("1"); + ecds->mutable_last_updated()->set_seconds(5); + + envoy::config::core::v3::TypedExtensionConfig filter_config; + filter_config.set_name("foo"); + auto listener_config = test::integration::filters::TestTcpListenerFilterConfig(); + listener_config.set_drain_bytes(5); + filter_config.mutable_typed_config()->PackFrom(listener_config); + ecds->mutable_ecds_filter()->PackFrom(filter_config); + return msg; +} + +TEST_P(AdminInstanceTest, ConfigDumpEcds) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto ecds_config = admin_.getConfigTracker().add("ecds", testDumpEcdsConfig); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.EcdsConfigDump.EcdsFilterConfig", + "version_info": "1", + "ecds_filter": { + "@type": "type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig", + "name": "foo", + "typed_config": { + "@type": "type.googleapis.com/test.integration.filters.TestTcpListenerFilterConfig", + "drain_bytes": 5 + } + }, + "last_updated": "1970-01-01T00:00:05Z" + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, + getCallback("/config_dump?resource=ecds_filters", header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + +TEST_P(AdminInstanceTest, ConfigDumpEcdsByResourceAndMask) { + Buffer::OwnedImpl response; + Http::TestResponseHeaderMapImpl header_map; + auto ecds_config = admin_.getConfigTracker().add("ecds", testDumpEcdsConfig); + const std::string expected_json = R"EOF({ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.EcdsConfigDump.EcdsFilterConfig", + "version_info": "1", + "ecds_filter": { + "@type": "type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig", + "name": "foo" + } + } + ] +} +)EOF"; + EXPECT_EQ(Http::Code::OK, getCallback("/config_dump?resource=ecds_filters&mask=" + "ecds_filter.name,version_info", + header_map, response)); + std::string output = response.toString(); + EXPECT_EQ(expected_json, output); +} + } // namespace Server } // namespace Envoy