Skip to content

Commit

Permalink
listener: first implementation of an Api Listener (#9516)
Browse files Browse the repository at this point in the history
Description: this PR introduces the initial implementation of an Api Listener based on the proto configuration merged in #8170. Notably, this PR introduces the ability to add only _one_ api listener via _bootstrap config only_. This decision was made in order to iterate into more complex setups (multiple listeners, LDS supplied listeners) in subsequent PRs. Moreover, the API listener is created in the context of Envoy's main thread not worker threads.

A first use of this Api Listener can be seen in envoyproxy/envoy-mobile#616.
Risk Level: low, only used in Envoy Mobile. The risk here is about building something generally useful and flexible. Note however that a couple of things were rejiggered in the HCM.
Testing: unit and integration tests. Additional testing in https://github.com/lyft/envoy-mobile.
Docs Changes: Added inline comments and TODOs. Proto documentation is up-to-date.
Release Notes: similar to doc changes.

Signed-off-by: Jose Nino <[email protected]>
  • Loading branch information
junr03 authored Jan 17, 2020
1 parent cb469c0 commit 9b6260f
Show file tree
Hide file tree
Showing 36 changed files with 1,053 additions and 39 deletions.
7 changes: 6 additions & 1 deletion api/envoy/api/v2/listener.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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<envoy_api_field_Listener.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,
Expand Down
3 changes: 1 addition & 2 deletions api/envoy/config/listener/v2/api_listener.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions api/envoy/config/listener/v3/api_listener.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion api/envoy/config/listener/v3/listener.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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<envoy_api_field_config.listener.v3.Listener.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,
Expand Down
7 changes: 6 additions & 1 deletion generated_api_shadow/envoy/api/v2/listener.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion generated_api_shadow/envoy/config/listener/v3/listener.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions include/envoy/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
33 changes: 33 additions & 0 deletions include/envoy/http/api_listener.h
Original file line number Diff line number Diff line change
@@ -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<ApiListener>;
using ApiListenerOptRef = absl::optional<std::reference_wrapper<ApiListener>>;

} // namespace Http
} // namespace Envoy
7 changes: 7 additions & 0 deletions include/envoy/server/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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",
Expand Down
39 changes: 39 additions & 0 deletions include/envoy/server/api_listener.h
Original file line number Diff line number Diff line change
@@ -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<ApiListener>;
using ApiListenerOptRef = absl::optional<std::reference_wrapper<ApiListener>>;

} // namespace Server
} // namespace Envoy
9 changes: 9 additions & 0 deletions include/envoy/server/listener_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -212,6 +213,14 @@ class ListenerManager {
*/
using FailureStates = std::vector<std::unique_ptr<envoy::admin::v3::UpdateFailureState>>;
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
Expand Down
1 change: 1 addition & 0 deletions source/common/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
35 changes: 23 additions & 12 deletions source/common/http/conn_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion source/common/http/conn_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -48,7 +49,8 @@ namespace Http {
class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
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,
Expand All @@ -66,6 +68,15 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
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;
Expand Down
16 changes: 16 additions & 0 deletions source/common/protobuf/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class MessageType>
static inline MessageType
anyConvertAndValidate(const ProtobufWkt::Any& message,
ProtobufMessage::ValidationVisitor& validation_visitor) {
MessageType typed_message = anyConvert<MessageType>(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 9b6260f

Please sign in to comment.