diff --git a/api/envoy/api/v2/listener.proto b/api/envoy/api/v2/listener.proto index 98c1efff0018f..3fbb10070d075 100644 --- a/api/envoy/api/v2/listener.proto +++ b/api/envoy/api/v2/listener.proto @@ -202,11 +202,16 @@ message Listener { // creating a packet-oriented UDP listener. If not present, treat it as "raw_udp_listener". listener.UdpListenerConfig udp_listener_config = 18; - // [#not-implemented-hide:] // Used to represent an API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. // When this field is set, no other field except for :ref:`name` // should be set. + // + // .. note:: + // + // Currently only one ApiListener can be installed; and it can only be done via bootstrap config, + // not LDS. + // // [#next-major-version: In the v3 API, instead of this messy approach where the socket // listener fields are directly in the top-level Listener message and the API listener types // are in the ApiListener message, the socket listener messages should be in their own message, diff --git a/api/envoy/config/listener/v2/api_listener.proto b/api/envoy/config/listener/v2/api_listener.proto index 48c99dc4e214e..0cdcd09131e08 100644 --- a/api/envoy/config/listener/v2/api_listener.proto +++ b/api/envoy/config/listener/v2/api_listener.proto @@ -11,13 +11,12 @@ option java_outer_classname = "ApiListenerProto"; option java_multiple_files = true; option (udpa.annotations.file_migrate).move_to_package = "envoy.config.listener.v3"; -// [#not-implemented-hide:] // Describes a type of API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. message ApiListener { // The type in this field determines the type of API listener. At present, the following // types are supported: - // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) + // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) // [#next-major-version: In the v3 API, replace this Any field with a oneof containing the // specific config message for each type of API listener. We could not do this in v2 because // it would have caused circular dependencies for go protos: lds.proto depends on this file, diff --git a/api/envoy/config/listener/v3/api_listener.proto b/api/envoy/config/listener/v3/api_listener.proto index 98f01a8376421..196d0c7b65c0c 100644 --- a/api/envoy/config/listener/v3/api_listener.proto +++ b/api/envoy/config/listener/v3/api_listener.proto @@ -10,7 +10,6 @@ option java_package = "io.envoyproxy.envoy.config.listener.v3"; option java_outer_classname = "ApiListenerProto"; option java_multiple_files = true; -// [#not-implemented-hide:] // Describes a type of API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. message ApiListener { @@ -19,7 +18,7 @@ message ApiListener { // The type in this field determines the type of API listener. At present, the following // types are supported: - // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) + // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) // [#next-major-version: In the v3 API, replace this Any field with a oneof containing the // specific config message for each type of API listener. We could not do this in v2 because // it would have caused circular dependencies for go protos: lds.proto depends on this file, diff --git a/api/envoy/config/listener/v3/listener.proto b/api/envoy/config/listener/v3/listener.proto index f6550df09cad3..9ea16084b2f44 100644 --- a/api/envoy/config/listener/v3/listener.proto +++ b/api/envoy/config/listener/v3/listener.proto @@ -196,11 +196,16 @@ message Listener { // for creating a packet-oriented UDP listener. If not present, treat it as "raw_udp_listener". UdpListenerConfig udp_listener_config = 18; - // [#not-implemented-hide:] // Used to represent an API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. // When this field is set, no other field except for // :ref:`name` should be set. + // + // .. note:: + // + // Currently only one ApiListener can be installed; and it can only be done via bootstrap config, + // not LDS. + // // [#next-major-version: In the v3 API, instead of this messy approach where the socket // listener fields are directly in the top-level Listener message and the API listener types // are in the ApiListener message, the socket listener messages should be in their own message, diff --git a/generated_api_shadow/envoy/api/v2/listener.proto b/generated_api_shadow/envoy/api/v2/listener.proto index 98c1efff0018f..3fbb10070d075 100644 --- a/generated_api_shadow/envoy/api/v2/listener.proto +++ b/generated_api_shadow/envoy/api/v2/listener.proto @@ -202,11 +202,16 @@ message Listener { // creating a packet-oriented UDP listener. If not present, treat it as "raw_udp_listener". listener.UdpListenerConfig udp_listener_config = 18; - // [#not-implemented-hide:] // Used to represent an API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. // When this field is set, no other field except for :ref:`name` // should be set. + // + // .. note:: + // + // Currently only one ApiListener can be installed; and it can only be done via bootstrap config, + // not LDS. + // // [#next-major-version: In the v3 API, instead of this messy approach where the socket // listener fields are directly in the top-level Listener message and the API listener types // are in the ApiListener message, the socket listener messages should be in their own message, diff --git a/generated_api_shadow/envoy/config/listener/v2/api_listener.proto b/generated_api_shadow/envoy/config/listener/v2/api_listener.proto index 48c99dc4e214e..0cdcd09131e08 100644 --- a/generated_api_shadow/envoy/config/listener/v2/api_listener.proto +++ b/generated_api_shadow/envoy/config/listener/v2/api_listener.proto @@ -11,13 +11,12 @@ option java_outer_classname = "ApiListenerProto"; option java_multiple_files = true; option (udpa.annotations.file_migrate).move_to_package = "envoy.config.listener.v3"; -// [#not-implemented-hide:] // Describes a type of API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. message ApiListener { // The type in this field determines the type of API listener. At present, the following // types are supported: - // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) + // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) // [#next-major-version: In the v3 API, replace this Any field with a oneof containing the // specific config message for each type of API listener. We could not do this in v2 because // it would have caused circular dependencies for go protos: lds.proto depends on this file, diff --git a/generated_api_shadow/envoy/config/listener/v3/api_listener.proto b/generated_api_shadow/envoy/config/listener/v3/api_listener.proto index 98f01a8376421..196d0c7b65c0c 100644 --- a/generated_api_shadow/envoy/config/listener/v3/api_listener.proto +++ b/generated_api_shadow/envoy/config/listener/v3/api_listener.proto @@ -10,7 +10,6 @@ option java_package = "io.envoyproxy.envoy.config.listener.v3"; option java_outer_classname = "ApiListenerProto"; option java_multiple_files = true; -// [#not-implemented-hide:] // Describes a type of API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. message ApiListener { @@ -19,7 +18,7 @@ message ApiListener { // The type in this field determines the type of API listener. At present, the following // types are supported: - // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) + // envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager (HTTP) // [#next-major-version: In the v3 API, replace this Any field with a oneof containing the // specific config message for each type of API listener. We could not do this in v2 because // it would have caused circular dependencies for go protos: lds.proto depends on this file, diff --git a/generated_api_shadow/envoy/config/listener/v3/listener.proto b/generated_api_shadow/envoy/config/listener/v3/listener.proto index 3090dc36e9d95..767fe197e94d5 100644 --- a/generated_api_shadow/envoy/config/listener/v3/listener.proto +++ b/generated_api_shadow/envoy/config/listener/v3/listener.proto @@ -212,11 +212,16 @@ message Listener { // for creating a packet-oriented UDP listener. If not present, treat it as "raw_udp_listener". UdpListenerConfig udp_listener_config = 18; - // [#not-implemented-hide:] // Used to represent an API listener, which is used in non-proxy clients. The type of API // exposed to the non-proxy application depends on the type of API listener. // When this field is set, no other field except for // :ref:`name` should be set. + // + // .. note:: + // + // Currently only one ApiListener can be installed; and it can only be done via bootstrap config, + // not LDS. + // // [#next-major-version: In the v3 API, instead of this messy approach where the socket // listener fields are directly in the top-level Listener message and the API listener types // are in the ApiListener message, the socket listener messages should be in their own message, diff --git a/include/envoy/http/BUILD b/include/envoy/http/BUILD index ecfc6c07c47dd..36217efeeeb81 100644 --- a/include/envoy/http/BUILD +++ b/include/envoy/http/BUILD @@ -8,6 +8,12 @@ load( envoy_package() +envoy_cc_library( + name = "api_listener_interface", + hdrs = ["api_listener.h"], + deps = [":codec_interface"], +) + envoy_cc_library( name = "async_client_interface", hdrs = ["async_client.h"], diff --git a/include/envoy/http/api_listener.h b/include/envoy/http/api_listener.h new file mode 100644 index 0000000000000..59c83a5dd93f5 --- /dev/null +++ b/include/envoy/http/api_listener.h @@ -0,0 +1,33 @@ +#pragma once + +#include "envoy/http/codec.h" + +namespace Envoy { +namespace Http { + +/** + * ApiListener that allows consumers to interact with HTTP streams via API calls. + */ +// TODO(junr03): this is a replica of the functions in ServerConnectionCallbacks. It would be nice +// to not duplicate this interface layout. +class ApiListener { +public: + virtual ~ApiListener() = default; + + /** + * Invoked when a new request stream is initiated by the remote. + * @param response_encoder supplies the encoder to use for creating the response. The request and + * response are backed by the same Stream object. + * @param is_internally_created indicates if this stream was originated by a + * client, or was created by Envoy, by example as part of an internal redirect. + * @return StreamDecoder& supplies the decoder callbacks to fire into for stream decoding events. + */ + virtual StreamDecoder& newStream(StreamEncoder& response_encoder, + bool is_internally_created = false) PURE; +}; + +using ApiListenerPtr = std::unique_ptr; +using ApiListenerOptRef = absl::optional>; + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/include/envoy/server/BUILD b/include/envoy/server/BUILD index 474bc262736ee..e48700dee94b4 100644 --- a/include/envoy/server/BUILD +++ b/include/envoy/server/BUILD @@ -31,6 +31,12 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "api_listener_interface", + hdrs = ["api_listener.h"], + deps = ["//include/envoy/http:api_listener_interface"], +) + envoy_cc_library( name = "configuration_interface", hdrs = ["configuration.h"], @@ -189,6 +195,7 @@ envoy_cc_library( name = "listener_manager_interface", hdrs = ["listener_manager.h"], deps = [ + ":api_listener_interface", ":drain_manager_interface", ":filter_config_interface", ":guarddog_interface", diff --git a/include/envoy/server/api_listener.h b/include/envoy/server/api_listener.h new file mode 100644 index 0000000000000..da5937f5d680a --- /dev/null +++ b/include/envoy/server/api_listener.h @@ -0,0 +1,39 @@ +#pragma once + +#include "envoy/http/api_listener.h" + +namespace Envoy { +namespace Server { + +/** + * Listener that allows consumer to interact with Envoy via a designated API. + */ +class ApiListener { +public: + enum class Type { HttpApiListener }; + + virtual ~ApiListener() = default; + + /** + * An ApiListener is uniquely identified by its name. + * + * @return the name of the ApiListener. + */ + virtual absl::string_view name() const PURE; + + /** + * @return the Type of the ApiListener. + */ + virtual Type type() const PURE; + + /** + * @return valid ref IFF type() == Type::HttpApiListener, otherwise nullopt. + */ + virtual Http::ApiListenerOptRef http() PURE; +}; + +using ApiListenerPtr = std::unique_ptr; +using ApiListenerOptRef = absl::optional>; + +} // namespace Server +} // namespace Envoy \ No newline at end of file diff --git a/include/envoy/server/listener_manager.h b/include/envoy/server/listener_manager.h index 58a0a488e2ebc..57a7e97549a27 100644 --- a/include/envoy/server/listener_manager.h +++ b/include/envoy/server/listener_manager.h @@ -9,6 +9,7 @@ #include "envoy/network/filter.h" #include "envoy/network/listen_socket.h" #include "envoy/network/listener.h" +#include "envoy/server/api_listener.h" #include "envoy/server/drain_manager.h" #include "envoy/server/filter_config.h" #include "envoy/server/guarddog.h" @@ -212,6 +213,14 @@ class ListenerManager { */ using FailureStates = std::vector>; virtual void endListenerUpdate(FailureStates&& failure_states) PURE; + + // TODO(junr03): once ApiListeners support warming and draining, this function should return a + // weak_ptr to its caller. This would allow the caller to verify if the + // ApiListener is available to receive API calls on it. + /** + * @return the server's API Listener if it exists, nullopt if it does not. + */ + virtual ApiListenerOptRef apiListener() PURE; }; } // namespace Server diff --git a/source/common/http/BUILD b/source/common/http/BUILD index 9624c9f424e9d..e4a2634fa59b8 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -162,6 +162,7 @@ envoy_cc_library( "//include/envoy/common:time_interface", "//include/envoy/event:deferred_deletable", "//include/envoy/event:dispatcher_interface", + "//include/envoy/http:api_listener_interface", "//include/envoy/http:codec_interface", "//include/envoy/http:context_interface", "//include/envoy/http:filter_interface", diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 57304b7b3abb5..4f730bf730335 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -286,18 +286,31 @@ void ConnectionManagerImpl::handleCodecException(const char* error) { read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWriteAndDelay); } +void ConnectionManagerImpl::createCodec(Buffer::Instance& data) { + ASSERT(!codec_); + codec_ = config_.createCodec(read_callbacks_->connection(), data, *this); + + switch (codec_->protocol()) { + case Protocol::Http3: + stats_.named_.downstream_cx_http3_total_.inc(); + stats_.named_.downstream_cx_http3_active_.inc(); + break; + case Protocol::Http2: + stats_.named_.downstream_cx_http2_total_.inc(); + stats_.named_.downstream_cx_http2_active_.inc(); + break; + case Protocol::Http11: + case Protocol::Http10: + stats_.named_.downstream_cx_http1_total_.inc(); + stats_.named_.downstream_cx_http1_active_.inc(); + break; + } +} + Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool) { if (!codec_) { // Http3 codec should have been instantiated by now. - codec_ = config_.createCodec(read_callbacks_->connection(), data, *this); - if (codec_->protocol() == Protocol::Http2) { - stats_.named_.downstream_cx_http2_total_.inc(); - stats_.named_.downstream_cx_http2_active_.inc(); - } else { - ASSERT(codec_->protocol() != Protocol::Http3); - stats_.named_.downstream_cx_http1_total_.inc(); - stats_.named_.downstream_cx_http1_active_.inc(); - } + createCodec(data); } bool redispatch; @@ -357,10 +370,8 @@ Network::FilterStatus ConnectionManagerImpl::onNewConnection() { } // Only QUIC connection's stream_info_ specifies protocol. Buffer::OwnedImpl dummy; - codec_ = config_.createCodec(read_callbacks_->connection(), dummy, *this); + createCodec(dummy); ASSERT(codec_->protocol() == Protocol::Http3); - stats_.named_.downstream_cx_http3_total_.inc(); - stats_.named_.downstream_cx_http3_active_.inc(); // Stop iterating through each filters for QUIC. Currently a QUIC connection // only supports one filter, HCM, and bypasses the onData() interface. Because // QUICHE already handles de-multiplexing. diff --git a/source/common/http/conn_manager_impl.h b/source/common/http/conn_manager_impl.h index b23e42d256e3c..57a2d4209c026 100644 --- a/source/common/http/conn_manager_impl.h +++ b/source/common/http/conn_manager_impl.h @@ -11,6 +11,7 @@ #include "envoy/access_log/access_log.h" #include "envoy/common/scope_tracker.h" #include "envoy/event/deferred_deletable.h" +#include "envoy/http/api_listener.h" #include "envoy/http/codec.h" #include "envoy/http/codes.h" #include "envoy/http/context.h" @@ -48,7 +49,8 @@ namespace Http { class ConnectionManagerImpl : Logger::Loggable, public Network::ReadFilter, public ServerConnectionCallbacks, - public Network::ConnectionCallbacks { + public Network::ConnectionCallbacks, + public Http::ApiListener { public: ConnectionManagerImpl(ConnectionManagerConfig& config, const Network::DrainDecision& drain_close, Runtime::RandomGenerator& random_generator, Http::Context& http_context, @@ -66,6 +68,15 @@ class ConnectionManagerImpl : Logger::Loggable, Stats::Scope& scope); static const HeaderMapImpl& continueHeader(); + // Currently the ConnectionManager creates a codec lazily when either: + // a) onConnection for H3. + // b) onData for H1 and H2. + // With the introduction of ApiListeners, neither event occurs. This function allows consumer code + // to manually create a codec. + // TODO(junr03): consider passing a synthetic codec instead of creating once. The codec in the + // ApiListener case is solely used to determine the protocol version. + void createCodec(Buffer::Instance& data); + // Network::ReadFilter Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; Network::FilterStatus onNewConnection() override; diff --git a/source/common/protobuf/utility.h b/source/common/protobuf/utility.h index 76bd9042f5261..b2d4b828be1ac 100644 --- a/source/common/protobuf/utility.h +++ b/source/common/protobuf/utility.h @@ -303,6 +303,22 @@ class MessageUtil { return typed_message; }; + /** + * Convert and validate from google.protobuf.Any to a typed message. + * @param message source google.protobuf.Any message. + * + * @return MessageType the typed message inside the Any. + * @throw ProtoValidationException if the message does not satisfy its type constraints. + */ + template + static inline MessageType + anyConvertAndValidate(const ProtobufWkt::Any& message, + ProtobufMessage::ValidationVisitor& validation_visitor) { + MessageType typed_message = anyConvert(message); + validate(typed_message, validation_visitor); + return typed_message; + }; + /** * Convert between two protobufs via a JSON round-trip. This is used to translate arbitrary * messages to/from google.protobuf.Struct. diff --git a/source/extensions/filters/network/http_connection_manager/BUILD b/source/extensions/filters/network/http_connection_manager/BUILD index fb26b3ae6da1f..c566531c557e1 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -20,6 +20,7 @@ envoy_cc_extension( deps = [ "//include/envoy/config:config_provider_manager_interface", "//include/envoy/filesystem:filesystem_interface", + "//include/envoy/http:codec_interface", "//include/envoy/http:filter_interface", "//include/envoy/registry", "//include/envoy/router:route_config_provider_manager_interface", diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 6efb4f2d29f14..3848e7f486fbd 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -18,7 +18,6 @@ #include "common/common/fmt.h" #include "common/config/utility.h" #include "common/http/conn_manager_utility.h" -#include "common/http/date_provider_impl.h" #include "common/http/default_server_string.h" #include "common/http/http1/codec_impl.h" #include "common/http/http2/codec_impl.h" @@ -26,8 +25,6 @@ #include "common/http/http3/well_known_names.h" #include "common/http/utility.h" #include "common/protobuf/utility.h" -#include "common/router/rds_impl.h" -#include "common/router/scoped_rds.h" #include "common/runtime/runtime_impl.h" #include "common/tracing/http_tracer_impl.h" @@ -78,11 +75,7 @@ SINGLETON_MANAGER_REGISTRATION(date_provider); SINGLETON_MANAGER_REGISTRATION(route_config_provider_manager); SINGLETON_MANAGER_REGISTRATION(scoped_routes_config_provider_manager); -Network::FilterFactoryCb -HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped( - const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& - proto_config, - Server::Configuration::FactoryContext& context) { +Utility::Singletons Utility::createSingletons(Server::Configuration::FactoryContext& context) { std::shared_ptr date_provider = context.singletonManager().getTyped( SINGLETON_MANAGER_REGISTERED_NAME(date_provider), [&context] { @@ -104,16 +97,36 @@ HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped( context.admin(), *route_config_provider_manager); }); - std::shared_ptr filter_config(new HttpConnectionManagerConfig( - proto_config, context, *date_provider, *route_config_provider_manager, - *scoped_routes_config_provider_manager)); + return {date_provider, route_config_provider_manager, scoped_routes_config_provider_manager}; +} + +std::shared_ptr Utility::createConfig( + const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + proto_config, + Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider, + Router::RouteConfigProviderManager& route_config_provider_manager, + Config::ConfigProviderManager& scoped_routes_config_provider_manager) { + return std::make_shared(proto_config, context, date_provider, + route_config_provider_manager, + scoped_routes_config_provider_manager); +} + +Network::FilterFactoryCb +HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + proto_config, + Server::Configuration::FactoryContext& context) { + Utility::Singletons singletons = Utility::createSingletons(context); + + auto filter_config = Utility::createConfig(proto_config, context, *singletons.date_provider_, + *singletons.route_config_provider_manager_, + *singletons.scoped_routes_config_provider_manager_); // This lambda captures the shared_ptrs created above, thus preserving the // reference count. // Keep in mind the lambda capture list **doesn't** determine the destruction order, but it's fine // as these captured objects are also global singletons. - return [scoped_routes_config_provider_manager, route_config_provider_manager, date_provider, - filter_config, &context](Network::FilterManager& filter_manager) -> void { + return [singletons, filter_config, &context](Network::FilterManager& filter_manager) -> void { filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Http::ConnectionManagerImpl( *filter_config, context.drainDecision(), context.random(), context.httpContext(), context.runtime(), context.localInfo(), context.clusterManager(), @@ -500,6 +513,44 @@ const Network::Address::Instance& HttpConnectionManagerConfig::localAddress() { return *context_.localInfo().address(); } +std::function +HttpConnectionManagerFactory::createHttpConnectionManagerFactoryFromProto( + const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + proto_config, + Server::Configuration::FactoryContext& context, Network::ReadFilterCallbacks& read_callbacks) { + + Utility::Singletons singletons = Utility::createSingletons(context); + + auto filter_config = Utility::createConfig(proto_config, context, *singletons.date_provider_, + *singletons.route_config_provider_manager_, + *singletons.scoped_routes_config_provider_manager_); + + // This lambda captures the shared_ptrs created above, thus preserving the + // reference count. + // Keep in mind the lambda capture list **doesn't** determine the destruction order, but it's fine + // as these captured objects are also global singletons. + return [singletons, filter_config, &context, &read_callbacks]() -> Http::ApiListenerPtr { + auto conn_manager = std::make_unique( + *filter_config, context.drainDecision(), context.random(), context.httpContext(), + context.runtime(), context.localInfo(), context.clusterManager(), + &context.overloadManager(), context.dispatcher().timeSource()); + + // This factory creates a new ConnectionManagerImpl in the absence of its usual environment as + // an L4 filter, so this factory needs to take a few actions. + + // When a new connection is creating its filter chain it hydrates the factory with a filter + // manager which provides the ConnectionManager with its "read_callbacks". + conn_manager->initializeReadFilterCallbacks(read_callbacks); + + // When the connection first calls onData on the ConnectionManager, the ConnectionManager + // creates a codec. Here we force create a codec as onData will not be called. + Buffer::OwnedImpl dummy; + conn_manager->createCodec(dummy); + + return conn_manager; + }; +} + } // namespace HttpConnectionManager } // namespace NetworkFilters } // namespace Extensions diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index 46c156b7077c8..56993962cf2aa 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -15,7 +15,10 @@ #include "common/common/logger.h" #include "common/http/conn_manager_impl.h" +#include "common/http/date_provider_impl.h" #include "common/json/json_loader.h" +#include "common/router/rds_impl.h" +#include "common/router/scoped_rds.h" #include "extensions/filters/network/common/factory_base.h" #include "extensions/filters/network/well_known_names.h" @@ -202,6 +205,55 @@ class HttpConnectionManagerConfig : Logger::Loggable, static const uint64_t RequestTimeoutMs = 0; }; +/** + * Factory to create an HttpConnectionManager outside of a Network Filter Chain. + */ +class HttpConnectionManagerFactory { +public: + static std::function createHttpConnectionManagerFactoryFromProto( + const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + proto_config, + Server::Configuration::FactoryContext& context, Network::ReadFilterCallbacks& read_callbacks); +}; + +/** + * Utility class for shared logic between HTTP connection manager factories. + */ +class Utility { +public: + struct Singletons { + std::shared_ptr date_provider_; + std::shared_ptr route_config_provider_manager_; + std::shared_ptr + scoped_routes_config_provider_manager_; + }; + + /** + * Create/get singletons needed for config creation. + * + * @param context supplies the context used to create the singletons. + * @return Singletons struct containing all the singletons. + */ + static Singletons createSingletons(Server::Configuration::FactoryContext& context); + + /** + * Create the HttpConnectionManagerConfig. + * + * @param proto_config supplies the config to install. + * @param context supplies the context used to create the config. + * @param date_provider the singleton used in config creation. + * @param route_config_provider_manager the singleton used in config creation. + * @param scoped_routes_config_provider_manager the singleton used in config creation. + * @return a shared_ptr to the created config object. + */ + static std::shared_ptr createConfig( + const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + proto_config, + Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider, + Router::RouteConfigProviderManager& route_config_provider_manager, + Config::ConfigProviderManager& scoped_routes_config_provider_manager); +}; + } // namespace HttpConnectionManager } // namespace NetworkFilters } // namespace Extensions diff --git a/source/server/BUILD b/source/server/BUILD index 4d0c4447c9a4e..b8b6b625a0aec 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -159,6 +159,7 @@ envoy_cc_library( srcs = envoy_select_hot_restart(["hot_restarting_parent.cc"]), hdrs = envoy_select_hot_restart(["hot_restarting_parent.h"]), deps = [ + ":api_listener_lib", ":hot_restarting_base", ":listener_lib", "//source/common/memory:stats_lib", @@ -269,6 +270,35 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "api_listener_lib", + srcs = [ + "api_listener_impl.cc", + ], + hdrs = [ + "api_listener_impl.h", + ], + deps = [ + ":drain_manager_lib", + ":filter_chain_manager_lib", + ":listener_manager_impl", + "//include/envoy/network:connection_interface", + "//include/envoy/server:api_listener_interface", + "//include/envoy/server:filter_config_interface", + "//include/envoy/server:listener_manager_interface", + "//source/common/common:empty_string", + "//source/common/http:conn_manager_lib", + "//source/common/init:manager_lib", + "//source/common/network:resolver_lib", + "//source/common/stream_info:stream_info_lib", + "//source/extensions/filters/network/http_connection_manager:config", + "@envoy_api//envoy/api/v2:pkg_cc_proto", + "@envoy_api//envoy/api/v2/listener:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "listener_lib", srcs = [ @@ -302,12 +332,16 @@ envoy_cc_library( ], ) +# TODO(junr03): actually separate this lib from the listener and api listener lib. +# this can be done if the parent_ in the listener and the api listener becomes the ListenerManager interface. +# the issue right now is that the listener's reach into the listener manager's server_ instance variable. envoy_cc_library( name = "listener_manager_impl", srcs = [ "listener_manager_impl.cc", ], hdrs = [ + "api_listener_impl.h", "listener_impl.h", "listener_manager_impl.h", ], @@ -325,12 +359,14 @@ envoy_cc_library( "//include/envoy/server:worker_interface", "//source/common/config:utility_lib", "//source/common/config:version_converter_lib", + "//source/common/http:conn_manager_lib", "//source/common/init:manager_lib", "//source/common/network:listen_socket_lib", "//source/common/network:socket_option_factory_lib", "//source/common/network:utility_lib", "//source/common/protobuf:utility_lib", "//source/extensions/filters/listener:well_known_names", + "//source/extensions/filters/network/http_connection_manager:config", "//source/extensions/transport_sockets:well_known_names", "@envoy_api//envoy/admin/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", @@ -344,6 +380,7 @@ envoy_cc_library( hdrs = ["filter_chain_manager_impl.h"], deps = [ ":filter_chain_factory_context_callback", + "//include/envoy/server:instance_interface", "//include/envoy/server:listener_manager_interface", "//include/envoy/server:transport_socket_config_interface", "//source/common/common:empty_string", @@ -391,6 +428,7 @@ envoy_cc_library( ], deps = [ ":active_raw_udp_listener_config", + ":api_listener_lib", ":configuration_lib", ":connection_handler_lib", ":guarddog_lib", diff --git a/source/server/api_listener_impl.cc b/source/server/api_listener_impl.cc new file mode 100644 index 0000000000000..f990254683815 --- /dev/null +++ b/source/server/api_listener_impl.cc @@ -0,0 +1,48 @@ +#include "server/api_listener_impl.h" + +#include "envoy/api/v2/lds.pb.h" +#include "envoy/api/v2/listener/listener.pb.h" +#include "envoy/stats/scope.h" + +#include "common/http/conn_manager_impl.h" +#include "common/network/resolver_impl.h" +#include "common/protobuf/utility.h" + +#include "server/drain_manager_impl.h" +#include "server/listener_manager_impl.h" + +#include "extensions/filters/network/http_connection_manager/config.h" + +namespace Envoy { +namespace Server { + +ApiListenerImplBase::ApiListenerImplBase(const envoy::config::listener::v3::Listener& config, + ListenerManagerImpl& parent, const std::string& name) + : config_(config), parent_(parent), name_(name), + address_(Network::Address::resolveProtoAddress(config.address())), + global_scope_(parent_.server_.stats().createScope("")), + listener_scope_(parent_.server_.stats().createScope(fmt::format("listener.api.{}.", name_))), + factory_context_(parent_.server_, config_, *this, *global_scope_, *listener_scope_), + read_callbacks_(SyntheticReadCallbacks(*this)) {} + +HttpApiListener::HttpApiListener(const envoy::config::listener::v3::Listener& config, + ListenerManagerImpl& parent, const std::string& name) + : ApiListenerImplBase(config, parent, name) { + auto typed_config = MessageUtil::anyConvertAndValidate< + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager>( + config.api_listener().api_listener(), factory_context_.messageValidationVisitor()); + + http_connection_manager_factory_ = Envoy::Extensions::NetworkFilters::HttpConnectionManager:: + HttpConnectionManagerFactory::createHttpConnectionManagerFactoryFromProto( + typed_config, factory_context_, read_callbacks_); +} + +Http::ApiListenerOptRef HttpApiListener::http() { + if (!http_connection_manager_) { + http_connection_manager_ = http_connection_manager_factory_(); + } + return Http::ApiListenerOptRef(std::ref(*http_connection_manager_)); +} + +} // namespace Server +} // namespace Envoy diff --git a/source/server/api_listener_impl.h b/source/server/api_listener_impl.h new file mode 100644 index 0000000000000..3b5dfdcded010 --- /dev/null +++ b/source/server/api_listener_impl.h @@ -0,0 +1,170 @@ +#pragma once + +#include + +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/listener/v3/listener.pb.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/server/api_listener.h" +#include "envoy/server/drain_manager.h" +#include "envoy/server/filter_config.h" +#include "envoy/server/listener_manager.h" +#include "envoy/stats/scope.h" + +#include "common/common/empty_string.h" +#include "common/common/logger.h" +#include "common/http/conn_manager_impl.h" +#include "common/init/manager_impl.h" +#include "common/stream_info/stream_info_impl.h" + +#include "server/filter_chain_manager_impl.h" + +namespace Envoy { +namespace Server { + +class ListenerManagerImpl; + +/** + * Base class all ApiListeners. + */ +class ApiListenerImplBase : public ApiListener, + public Network::DrainDecision, + Logger::Loggable { +public: + // TODO(junr03): consider moving Envoy Mobile's SyntheticAddressImpl to Envoy in order to return + // that rather than this semi-real one. + const Network::Address::InstanceConstSharedPtr& address() const { return address_; } + + // ApiListener + absl::string_view name() const override { return name_; } + + // Network::DrainDecision + // TODO(junr03): hook up draining to listener state management. + bool drainClose() const override { return false; } + +protected: + ApiListenerImplBase(const envoy::config::listener::v3::Listener& config, + ListenerManagerImpl& parent, const std::string& name); + + // Synthetic class that acts as a stub Network::ReadFilterCallbacks. + // TODO(junr03): if we are able to separate the Network Filter aspects of the + // Http::ConnectionManagerImpl from the http management aspects of it, it is possible we would not + // need this and the SyntheticConnection stub anymore. + class SyntheticReadCallbacks : public Network::ReadFilterCallbacks { + public: + SyntheticReadCallbacks(ApiListenerImplBase& parent) + : parent_(parent), connection_(SyntheticConnection(*this)) {} + + // Network::ReadFilterCallbacks + void continueReading() override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + void injectReadDataToFilterChain(Buffer::Instance&, bool) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + Upstream::HostDescriptionConstSharedPtr upstreamHost() override { return nullptr; } + void upstreamHost(Upstream::HostDescriptionConstSharedPtr) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + Network::Connection& connection() override { return connection_; } + + // Synthetic class that acts as a stub for the connection backing the + // Network::ReadFilterCallbacks. + class SyntheticConnection : public Network::Connection { + public: + SyntheticConnection(SyntheticReadCallbacks& parent) + : parent_(parent), stream_info_(parent_.parent_.factory_context_.timeSource()), + options_(std::make_shared>()) {} + + // Network::FilterManager + void addWriteFilter(Network::WriteFilterSharedPtr) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + void addFilter(Network::FilterSharedPtr) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + void addReadFilter(Network::ReadFilterSharedPtr) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + bool initializeReadFilters() override { return true; } + + // Network::Connection + void addConnectionCallbacks(Network::ConnectionCallbacks&) override {} + void addBytesSentCallback(Network::Connection::BytesSentCb) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + void enableHalfClose(bool) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + void close(Network::ConnectionCloseType) override {} + Event::Dispatcher& dispatcher() override { + return parent_.parent_.factory_context_.dispatcher(); + } + uint64_t id() const override { return 12345; } + std::string nextProtocol() const override { return EMPTY_STRING; } + void noDelay(bool) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + void readDisable(bool) override {} + void detectEarlyCloseWhenReadDisabled(bool) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + bool readEnabled() const override { return true; } + const Network::Address::InstanceConstSharedPtr& remoteAddress() const override { + return parent_.parent_.address(); + } + absl::optional + unixSocketPeerCredentials() const override { + return absl::nullopt; + } + const Network::Address::InstanceConstSharedPtr& localAddress() const override { + return parent_.parent_.address(); + } + void setConnectionStats(const Network::Connection::ConnectionStats&) override {} + Ssl::ConnectionInfoConstSharedPtr ssl() const override { return nullptr; } + absl::string_view requestedServerName() const override { return EMPTY_STRING; } + State state() const override { return Network::Connection::State::Open; } + void write(Buffer::Instance&, bool) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + void setBufferLimits(uint32_t) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + uint32_t bufferLimit() const override { return 65000; } + bool localAddressRestored() const override { return false; } + bool aboveHighWatermark() const override { return false; } + const Network::ConnectionSocket::OptionsSharedPtr& socketOptions() const override { + return options_; + } + StreamInfo::StreamInfo& streamInfo() override { return stream_info_; } + const StreamInfo::StreamInfo& streamInfo() const override { return stream_info_; } + void setDelayedCloseTimeout(std::chrono::milliseconds) override {} + absl::string_view transportFailureReason() const override { return EMPTY_STRING; } + + SyntheticReadCallbacks& parent_; + StreamInfo::StreamInfoImpl stream_info_; + Network::ConnectionSocket::OptionsSharedPtr options_; + }; + + ApiListenerImplBase& parent_; + SyntheticConnection connection_; + }; + + const envoy::config::listener::v3::Listener& config_; + ListenerManagerImpl& parent_; + const std::string name_; + Network::Address::InstanceConstSharedPtr address_; + Stats::ScopePtr global_scope_; + Stats::ScopePtr listener_scope_; + FactoryContextImpl factory_context_; + SyntheticReadCallbacks read_callbacks_; +}; + +/** + * ApiListener that provides a handle to inject HTTP calls into Envoy via an + * Http::ConnectionManager. Thus, it provides full access to Envoy's L7 features, e.g HTTP filters. + */ +class HttpApiListener : public ApiListenerImplBase { +public: + HttpApiListener(const envoy::config::listener::v3::Listener& config, ListenerManagerImpl& parent, + const std::string& name); + + // ApiListener + ApiListener::Type type() const override { return ApiListener::Type::HttpApiListener; } + Http::ApiListenerOptRef http() override; + +private: + // Need to store the factory due to the shared_ptrs that need to be kept alive: date provider, + // route config manager, scoped route config manager. + std::function http_connection_manager_factory_; + // Http::ServerConnectionCallbacks is the API surface that this class provides via its handle(). + Http::ApiListenerPtr http_connection_manager_; +}; + +} // namespace Server +} // namespace Envoy diff --git a/source/server/filter_chain_manager_impl.cc b/source/server/filter_chain_manager_impl.cc index 329f25c507450..a84eb255f3ec0 100644 --- a/source/server/filter_chain_manager_impl.cc +++ b/source/server/filter_chain_manager_impl.cc @@ -575,5 +575,51 @@ Configuration::FilterChainFactoryContext& FilterChainManagerImpl::createFilterCh factory_contexts_.push_back(std::make_unique(parent_context_)); return *factory_contexts_.back(); } + +FactoryContextImpl::FactoryContextImpl(Server::Instance& server, + const envoy::config::listener::v3::Listener& config, + Network::DrainDecision& drain_decision, + Stats::Scope& global_scope, Stats::Scope& listener_scope) + : server_(server), config_(config), drain_decision_(drain_decision), + global_scope_(global_scope), listener_scope_(listener_scope) {} + +AccessLog::AccessLogManager& FactoryContextImpl::accessLogManager() { + return server_.accessLogManager(); +} +Upstream::ClusterManager& FactoryContextImpl::clusterManager() { return server_.clusterManager(); } +Event::Dispatcher& FactoryContextImpl::dispatcher() { return server_.dispatcher(); } +Grpc::Context& FactoryContextImpl::grpcContext() { return server_.grpcContext(); } +bool FactoryContextImpl::healthCheckFailed() { return server_.healthCheckFailed(); } +Tracing::HttpTracer& FactoryContextImpl::httpTracer() { return server_.httpContext().tracer(); } +Http::Context& FactoryContextImpl::httpContext() { return server_.httpContext(); } +Init::Manager& FactoryContextImpl::initManager() { return server_.initManager(); } +const LocalInfo::LocalInfo& FactoryContextImpl::localInfo() const { return server_.localInfo(); } +Envoy::Runtime::RandomGenerator& FactoryContextImpl::random() { return server_.random(); } +Envoy::Runtime::Loader& FactoryContextImpl::runtime() { return server_.runtime(); } +Stats::Scope& FactoryContextImpl::scope() { return global_scope_; } +Singleton::Manager& FactoryContextImpl::singletonManager() { return server_.singletonManager(); } +OverloadManager& FactoryContextImpl::overloadManager() { return server_.overloadManager(); } +ThreadLocal::SlotAllocator& FactoryContextImpl::threadLocal() { return server_.threadLocal(); } +Admin& FactoryContextImpl::admin() { return server_.admin(); } +TimeSource& FactoryContextImpl::timeSource() { return server_.timeSource(); } +ProtobufMessage::ValidationVisitor& FactoryContextImpl::messageValidationVisitor() { + return server_.messageValidationContext().staticValidationVisitor(); +} +Api::Api& FactoryContextImpl::api() { return server_.api(); } +ServerLifecycleNotifier& FactoryContextImpl::lifecycleNotifier() { + return server_.lifecycleNotifier(); +} +OptProcessContextRef FactoryContextImpl::processContext() { return server_.processContext(); } +Configuration::ServerFactoryContext& FactoryContextImpl::getServerFactoryContext() const { + return server_.serverFactoryContext(); +} +const envoy::config::core::v3::Metadata& FactoryContextImpl::listenerMetadata() const { + return config_.metadata(); +} +envoy::config::core::v3::TrafficDirection FactoryContextImpl::direction() const { + return config_.traffic_direction(); +} +Network::DrainDecision& FactoryContextImpl::drainDecision() { return drain_decision_; } +Stats::Scope& FactoryContextImpl::listenerScope() { return listener_scope_; } } // namespace Server } // namespace Envoy diff --git a/source/server/filter_chain_manager_impl.h b/source/server/filter_chain_manager_impl.h index 7659acf5b1678..dec926d3ba3ae 100644 --- a/source/server/filter_chain_manager_impl.h +++ b/source/server/filter_chain_manager_impl.h @@ -6,6 +6,7 @@ #include "envoy/config/listener/v3/listener_components.pb.h" #include "envoy/network/drain_decision.h" #include "envoy/server/filter_config.h" +#include "envoy/server/instance.h" #include "envoy/server/transport_socket_config.h" #include "envoy/thread_local/thread_local.h" @@ -73,6 +74,51 @@ class FilterChainFactoryContextImpl : public Configuration::FilterChainFactoryCo Configuration::FactoryContext& parent_context_; }; +/** + * Implementation of FactoryContext wrapping a Server::Instance and some listener components. + */ +class FactoryContextImpl : public Configuration::FactoryContext { +public: + FactoryContextImpl(Server::Instance& server, const envoy::config::listener::v3::Listener& config, + Network::DrainDecision& drain_decision, Stats::Scope& global_scope, + Stats::Scope& listener_scope); + + // Configuration::FactoryContext + AccessLog::AccessLogManager& accessLogManager() override; + Upstream::ClusterManager& clusterManager() override; + Event::Dispatcher& dispatcher() override; + Grpc::Context& grpcContext() override; + bool healthCheckFailed() override; + Tracing::HttpTracer& httpTracer() override; + Http::Context& httpContext() override; + Init::Manager& initManager() override; + const LocalInfo::LocalInfo& localInfo() const override; + Envoy::Runtime::RandomGenerator& random() override; + Envoy::Runtime::Loader& runtime() override; + Stats::Scope& scope() override; + Singleton::Manager& singletonManager() override; + OverloadManager& overloadManager() override; + ThreadLocal::SlotAllocator& threadLocal() override; + Admin& admin() override; + TimeSource& timeSource() override; + ProtobufMessage::ValidationVisitor& messageValidationVisitor() override; + Api::Api& api() override; + ServerLifecycleNotifier& lifecycleNotifier() override; + OptProcessContextRef processContext() override; + Configuration::ServerFactoryContext& getServerFactoryContext() const override; + const envoy::config::core::v3::Metadata& listenerMetadata() const override; + envoy::config::core::v3::TrafficDirection direction() const override; + Network::DrainDecision& drainDecision() override; + Stats::Scope& listenerScope() override; + +private: + Server::Instance& server_; + const envoy::config::listener::v3::Listener& config_; + Network::DrainDecision& drain_decision_; + Stats::Scope& global_scope_; + Stats::Scope& listener_scope_; +}; + /** * Implementation of FilterChainManager. */ diff --git a/source/server/listener_manager_impl.cc b/source/server/listener_manager_impl.cc index edcc9132662fc..c9e289291f1a9 100644 --- a/source/server/listener_manager_impl.cc +++ b/source/server/listener_manager_impl.cc @@ -22,6 +22,7 @@ #include "common/network/utility.h" #include "common/protobuf/utility.h" +#include "server/api_listener_impl.h" #include "server/configuration_impl.h" #include "server/drain_manager_impl.h" #include "server/filter_chain_manager_impl.h" @@ -319,6 +320,23 @@ ListenerManagerStats ListenerManagerImpl::generateStats(Stats::Scope& scope) { bool ListenerManagerImpl::addOrUpdateListener(const envoy::config::listener::v3::Listener& config, const std::string& version_info, bool added_via_api) { + + // TODO(junr03): currently only one ApiListener can be installed via bootstrap to avoid having to + // build a collection of listeners, and to have to be able to warm and drain the listeners. In the + // future allow multiple ApiListeners, and allow them to be created via LDS as well as bootstrap. + if (config.has_api_listener()) { + if (!api_listener_ && !added_via_api) { + // TODO(junr03): dispatch to different concrete constructors when there are other + // ApiListenerImplBase derived classes. + api_listener_ = std::make_unique(config, *this, config.name()); + return true; + } else { + ENVOY_LOG(warn, "listener {} can not be added because currently only one ApiListener is " + "allowed, and it can only be added via bootstrap configuration"); + return false; + } + } + std::string name; if (!config.name().empty()) { name = config.name(); @@ -838,5 +856,9 @@ Network::ListenSocketFactorySharedPtr ListenerManagerImpl::createListenSocketFac listener.bindToPort(), listener.name(), reuse_port); } +ApiListenerOptRef ListenerManagerImpl::apiListener() { + return api_listener_ ? ApiListenerOptRef(std::ref(*api_listener_)) : absl::nullopt; +} + } // namespace Server } // namespace Envoy diff --git a/source/server/listener_manager_impl.h b/source/server/listener_manager_impl.h index 1744453ffee4a..6b24610fff7d1 100644 --- a/source/server/listener_manager_impl.h +++ b/source/server/listener_manager_impl.h @@ -10,6 +10,7 @@ #include "envoy/config/listener/v3/listener_components.pb.h" #include "envoy/network/filter.h" #include "envoy/network/listen_socket.h" +#include "envoy/server/api_listener.h" #include "envoy/server/filter_config.h" #include "envoy/server/instance.h" #include "envoy/server/listener_manager.h" @@ -149,6 +150,7 @@ class ListenerManagerImpl : public ListenerManager, Logger::Loggable { +public: + ApiListenerIntegrationTest() : BaseIntegrationTest(GetParam(), bootstrap_config()) { + use_lds_ = false; + autonomous_upstream_ = true; + } + + void SetUp() override { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // currently ApiListener does not trigger this wait + // https://github.com/envoyproxy/envoy/blob/0b92c58d08d28ba7ef0ed5aaf44f90f0fccc5dce/test/integration/integration.cc#L454 + // Thus, the ApiListener has to be added in addition to the already existing listener in the + // config. + bootstrap.mutable_static_resources()->add_listeners()->MergeFrom( + Server::parseListenerFromV2Yaml(api_listener_config())); + }); + BaseIntegrationTest::initialize(); + } + + void TearDown() override { + test_server_.reset(); + fake_upstreams_.clear(); + } + + static std::string bootstrap_config() { + // At least one empty filter chain needs to be specified. + return ConfigHelper::BASE_CONFIG + R"EOF( + filter_chains: + filters: + )EOF"; + } + + static std::string api_listener_config() { + return R"EOF( +name: api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: hcm + route_config: + virtual_hosts: + name: integration + routes: + route: + cluster: cluster_0 + match: + prefix: "/" + domains: "*" + name: route_config_0 + http_filters: + - name: envoy.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + } + + NiceMock stream_encoder_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, ApiListenerIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(ApiListenerIntegrationTest, Basic) { + ConditionalInitializer test_ran; + test_server_->server().dispatcher().post([this, &test_ran]() -> void { + ASSERT_TRUE(test_server_->server().listenerManager().apiListener().has_value()); + ASSERT_EQ("api_listener", test_server_->server().listenerManager().apiListener()->get().name()); + ASSERT_TRUE(test_server_->server().listenerManager().apiListener()->get().http().has_value()); + auto& http_api_listener = + test_server_->server().listenerManager().apiListener()->get().http()->get(); + + ON_CALL(stream_encoder_, getStream()).WillByDefault(ReturnRef(stream_encoder_.stream_)); + auto& stream_decoder = http_api_listener.newStream(stream_encoder_); + + // The AutonomousUpstream responds with 200 OK and a body of 10 bytes. + // In the http1 codec the end stream is encoded with encodeData and 0 bytes. + Http::TestHeaderMapImpl expected_response_headers{{":status", "200"}}; + EXPECT_CALL(stream_encoder_, encodeHeaders(_, false)); + EXPECT_CALL(stream_encoder_, encodeData(_, false)); + EXPECT_CALL(stream_encoder_, encodeData(BufferStringEqual(""), true)); + + // Send a headers-only request + stream_decoder.decodeHeaders( + Http::HeaderMapPtr(new Http::TestHeaderMapImpl{ + {":method", "GET"}, {":path", "/api"}, {":scheme", "http"}, {":authority", "host"}}), + true); + + test_ran.setReady(); + }); + test_ran.waitReady(); +} + +} // namespace +} // namespace Envoy \ No newline at end of file diff --git a/test/mocks/http/BUILD b/test/mocks/http/BUILD index 7c0245453ac02..34227d7ac58ba 100644 --- a/test/mocks/http/BUILD +++ b/test/mocks/http/BUILD @@ -9,6 +9,15 @@ load( envoy_package() +envoy_cc_mock( + name = "api_listener_mocks", + srcs = ["api_listener.cc"], + hdrs = ["api_listener.h"], + deps = [ + "//include/envoy/http:api_listener_interface", + ], +) + envoy_cc_mock( name = "conn_pool_mocks", srcs = ["conn_pool.cc"], diff --git a/test/mocks/http/api_listener.cc b/test/mocks/http/api_listener.cc new file mode 100644 index 0000000000000..975aa1633da96 --- /dev/null +++ b/test/mocks/http/api_listener.cc @@ -0,0 +1,10 @@ +#include "test/mocks/http/api_listener.h" + +namespace Envoy { +namespace Http { + +MockApiListener::MockApiListener() = default; +MockApiListener::~MockApiListener() = default; + +} // namespace Http +} // namespace Envoy diff --git a/test/mocks/http/api_listener.h b/test/mocks/http/api_listener.h new file mode 100644 index 0000000000000..e212202a8e676 --- /dev/null +++ b/test/mocks/http/api_listener.h @@ -0,0 +1,20 @@ +#pragma once +#include "envoy/http/api_listener.h" + +#include "gmock/gmock.h" + +namespace Envoy { +namespace Http { + +class MockApiListener : public ApiListener { +public: + MockApiListener(); + ~MockApiListener() override; + + // Http::ApiListener + MOCK_METHOD2(newStream, + StreamDecoder&(StreamEncoder& response_encoder, bool is_internally_created)); +}; + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/test/mocks/server/mocks.h b/test/mocks/server/mocks.h index c94e4deb13bd1..c505cc2aea659 100644 --- a/test/mocks/server/mocks.h +++ b/test/mocks/server/mocks.h @@ -290,6 +290,7 @@ class MockListenerManager : public ListenerManager { MOCK_METHOD0(stopWorkers, void()); MOCK_METHOD0(beginListenerUpdate, void()); MOCK_METHOD1(endListenerUpdate, void(ListenerManager::FailureStates&&)); + MOCK_METHOD0(apiListener, ApiListenerOptRef()); }; class MockServerLifecycleNotifier : public ServerLifecycleNotifier { diff --git a/test/server/BUILD b/test/server/BUILD index 99b9efd2b3caa..6ca389536809d 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -16,6 +16,19 @@ load( envoy_package() +envoy_cc_test( + name = "api_listener_test", + srcs = ["api_listener_test.cc"], + deps = [ + ":utility_lib", + "//source/server:api_listener_lib", + "//source/server:listener_lib", + "//test/mocks/server:server_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "backtrace_test", srcs = ["backtrace_test.cc"], @@ -183,6 +196,7 @@ envoy_cc_test_library( hdrs = ["listener_manager_impl_test.h"], data = ["//test/extensions/transport_sockets/tls/test_data:certs"], deps = [ + "//source/server:api_listener_lib", "//source/server:listener_lib", "//test/mocks/network:network_mocks", "//test/mocks/server:server_mocks", @@ -259,6 +273,7 @@ envoy_cc_test( "//source/extensions/transport_sockets/raw_buffer:config", "//source/extensions/transport_sockets/tls:config", "//source/extensions/transport_sockets/tls:ssl_socket_lib", + "//source/server:api_listener_lib", "//source/server:filter_chain_manager_lib", "//source/server:listener_lib", "//test/mocks/network:network_mocks", diff --git a/test/server/api_listener_test.cc b/test/server/api_listener_test.cc new file mode 100644 index 0000000000000..ccf9c643ee551 --- /dev/null +++ b/test/server/api_listener_test.cc @@ -0,0 +1,91 @@ +#include + +#include "envoy/config/listener/v3/listener.pb.h" + +#include "server/api_listener_impl.h" +#include "server/listener_manager_impl.h" + +#include "test/mocks/server/mocks.h" +#include "test/server/utility.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Server { + +class ApiListenerTest : public testing::Test { +protected: + ApiListenerTest() + : listener_manager_(std::make_unique(server_, listener_factory_, + worker_factory_, false)) {} + + NiceMock server_; + NiceMock listener_factory_; + NiceMock worker_factory_; + std::unique_ptr listener_manager_; +}; + +TEST_F(ApiListenerTest, HttpApiListener) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV2Yaml(yaml); + + auto http_api_listener = HttpApiListener(config, *listener_manager_, config.name()); + + ASSERT_EQ("test_api_listener", http_api_listener.name()); + ASSERT_EQ(ApiListener::Type::HttpApiListener, http_api_listener.type()); + ASSERT_TRUE(http_api_listener.http().has_value()); +} + +TEST_F(ApiListenerTest, HttpApiListenerThrowsWithBadConfig) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.api.v2.Cluster + name: cluster1 + type: EDS + eds_cluster_config: + eds_config: + path: eds path + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV2Yaml(yaml); + + EXPECT_THROW_WITH_MESSAGE( + HttpApiListener(config, *listener_manager_, config.name()), EnvoyException, + "Unable to unpack as " + "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager: " + "[type.googleapis.com/envoy.api.v2.Cluster] {\n name: \"cluster1\"\n type: EDS\n " + "eds_cluster_config {\n eds_config {\n path: \"eds path\"\n }\n }\n}\n"); +} + +} // namespace Server +} // namespace Envoy \ No newline at end of file diff --git a/test/server/listener_manager_impl_test.cc b/test/server/listener_manager_impl_test.cc index 91c6b89acbb60..149cd04428e10 100644 --- a/test/server/listener_manager_impl_test.cc +++ b/test/server/listener_manager_impl_test.cc @@ -3611,7 +3611,7 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, VerifySanWithNoCA) { - certificate_chain: { filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_dns_cert.pem" } private_key: { filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_dns_key.pem" } validation_context: - match_subject_alt_names: + match_subject_alt_names: exact: "spiffe://lyft.com/testclient" )EOF", Network::Address::IpVersion::v4); @@ -3677,6 +3677,124 @@ TEST_F(ListenerManagerImplWithDispatcherStatsTest, DispatherStatsWithCorrectPref manager_->startWorkers(guard_dog_); } +TEST_F(ListenerManagerImplWithRealFiltersTest, ApiListener) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + ASSERT_TRUE(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), "", false)); + EXPECT_EQ(0U, manager_->listeners().size()); + ASSERT_TRUE(manager_->apiListener().has_value()); +} + +TEST_F(ListenerManagerImplWithRealFiltersTest, ApiListenerNotAllowedAddedViaApi) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + ASSERT_FALSE(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), "", true)); + EXPECT_EQ(0U, manager_->listeners().size()); + ASSERT_FALSE(manager_->apiListener().has_value()); +} + +TEST_F(ListenerManagerImplWithRealFiltersTest, ApiListenerOnlyOneApiListener) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const std::string yaml2 = R"EOF( +name: test_api_listener_2 +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + ASSERT_TRUE(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), "", false)); + EXPECT_EQ(0U, manager_->listeners().size()); + ASSERT_TRUE(manager_->apiListener().has_value()); + EXPECT_EQ("test_api_listener", manager_->apiListener()->get().name()); + + // Only one ApiListener is added. + ASSERT_FALSE(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), "", false)); + EXPECT_EQ(0U, manager_->listeners().size()); + // The original ApiListener is there. + ASSERT_TRUE(manager_->apiListener().has_value()); + EXPECT_EQ("test_api_listener", manager_->apiListener()->get().name()); +} + } // namespace } // namespace Server } // namespace Envoy