From 82507802cbea7939e9c41b4135e4faf68cacfd42 Mon Sep 17 00:00:00 2001 From: Kuat Date: Thu, 23 Jul 2020 14:33:43 -0700 Subject: [PATCH] xds: implement extension config discovery for HCM (#11826) Signed-off-by: Kuat Yessenov Signed-off-by: Kevin Baichoo --- api/BUILD | 1 + api/envoy/config/accesslog/v3/accesslog.proto | 1 + .../config/accesslog/v4alpha/accesslog.proto | 1 + api/envoy/config/core/v3/extension.proto | 31 ++ api/envoy/config/core/v4alpha/extension.proto | 34 ++ api/envoy/data/accesslog/v3/accesslog.proto | 5 +- .../v3/http_connection_manager.proto | 34 +- .../v4alpha/http_connection_manager.proto | 38 +- .../service/{filter => extension}/v3/BUILD | 0 .../v3/config_discovery.proto} | 21 +- api/versioning/BUILD | 2 +- docs/root/api-v3/service/service.rst | 1 + .../root/configuration/overview/extension.rst | 22 + docs/root/version_history/current.rst | 2 + .../envoy/config/accesslog/v3/accesslog.proto | 1 + .../config/accesslog/v4alpha/accesslog.proto | 1 + .../envoy/config/core/v3/extension.proto | 31 ++ .../envoy/config/core/v4alpha/extension.proto | 34 ++ .../envoy/data/accesslog/v3/accesslog.proto | 5 +- .../v3/http_connection_manager.proto | 34 +- .../v4alpha/http_connection_manager.proto | 38 +- .../service/{filter => extension}/v3/BUILD | 0 .../v3/config_discovery.proto} | 21 +- include/envoy/config/BUILD | 6 + .../envoy/config/extension_config_provider.h | 53 +++ include/envoy/filter/http/BUILD | 21 + .../filter/http/filter_config_provider.h | 57 +++ include/envoy/stream_info/stream_info.h | 4 +- source/common/config/BUILD | 1 + source/common/config/protobuf_link_hacks.h | 1 + source/common/config/type_to_endpoint.cc | 7 + source/common/config/utility.h | 49 +- source/common/filter/http/BUILD | 31 ++ .../http/filter_config_discovery_impl.cc | 211 +++++++++ .../http/filter_config_discovery_impl.h | 184 ++++++++ source/common/stream_info/utility.cc | 8 +- source/common/stream_info/utility.h | 1 + .../grpc/grpc_access_log_utils.cc | 5 +- .../network/http_connection_manager/BUILD | 2 + .../network/http_connection_manager/config.cc | 142 +++++- .../network/http_connection_manager/config.h | 19 +- .../common/access_log/access_log_impl_test.cc | 12 +- test/common/filter/http/BUILD | 30 ++ .../http/filter_config_discovery_impl_test.cc | 297 ++++++++++++ test/common/stream_info/utility_test.cc | 10 +- .../grpc/grpc_access_log_utils_test.cc | 1 + .../network/http_connection_manager/BUILD | 1 + .../http_connection_manager/config_test.cc | 422 +++++++++++++++--- test/integration/BUILD | 15 + .../extension_discovery_integration_test.cc | 327 ++++++++++++++ 50 files changed, 2025 insertions(+), 250 deletions(-) rename api/envoy/service/{filter => extension}/v3/BUILD (100%) rename api/envoy/service/{filter/v3/filter_config_discovery.proto => extension/v3/config_discovery.proto} (52%) rename generated_api_shadow/envoy/service/{filter => extension}/v3/BUILD (100%) rename generated_api_shadow/envoy/service/{filter/v3/filter_config_discovery.proto => extension/v3/config_discovery.proto} (52%) create mode 100644 include/envoy/config/extension_config_provider.h create mode 100644 include/envoy/filter/http/BUILD create mode 100644 include/envoy/filter/http/filter_config_provider.h create mode 100644 source/common/filter/http/BUILD create mode 100644 source/common/filter/http/filter_config_discovery_impl.cc create mode 100644 source/common/filter/http/filter_config_discovery_impl.h create mode 100644 test/common/filter/http/BUILD create mode 100644 test/common/filter/http/filter_config_discovery_impl_test.cc create mode 100644 test/integration/extension_discovery_integration_test.cc diff --git a/api/BUILD b/api/BUILD index 9d4f802dfe5f..50835fb0b1c4 100644 --- a/api/BUILD +++ b/api/BUILD @@ -245,6 +245,7 @@ proto_library( "//envoy/service/discovery/v3:pkg", "//envoy/service/endpoint/v3:pkg", "//envoy/service/event_reporting/v3:pkg", + "//envoy/service/extension/v3:pkg", "//envoy/service/health/v3:pkg", "//envoy/service/listener/v3:pkg", "//envoy/service/load_stats/v3:pkg", diff --git a/api/envoy/config/accesslog/v3/accesslog.proto b/api/envoy/config/accesslog/v3/accesslog.proto index 9a2f276b34b4..e1b5a2e58b90 100644 --- a/api/envoy/config/accesslog/v3/accesslog.proto +++ b/api/envoy/config/accesslog/v3/accesslog.proto @@ -242,6 +242,7 @@ message ResponseFlagFilter { in: "DPE" in: "UMSDR" in: "RFCF" + in: "NFCF" } } }]; diff --git a/api/envoy/config/accesslog/v4alpha/accesslog.proto b/api/envoy/config/accesslog/v4alpha/accesslog.proto index 939d4df95889..35f494ea1ac8 100644 --- a/api/envoy/config/accesslog/v4alpha/accesslog.proto +++ b/api/envoy/config/accesslog/v4alpha/accesslog.proto @@ -241,6 +241,7 @@ message ResponseFlagFilter { in: "DPE" in: "UMSDR" in: "RFCF" + in: "NFCF" } } }]; diff --git a/api/envoy/config/core/v3/extension.proto b/api/envoy/config/core/v3/extension.proto index 636398760785..ba66da6a8e36 100644 --- a/api/envoy/config/core/v3/extension.proto +++ b/api/envoy/config/core/v3/extension.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.config.core.v3; +import "envoy/config/core/v3/config_source.proto"; + import "google/protobuf/any.proto"; import "udpa/annotations/status.proto"; @@ -28,3 +30,32 @@ message TypedExtensionConfig { // ` for further details. google.protobuf.Any typed_config = 2 [(validate.rules).any = {required: true}]; } + +// Configuration source specifier for a late-bound extension configuration. The +// parent resource is warmed until all the initial extension configurations are +// received, unless the flag to apply the default configuration is set. +// Subsequent extension updates are atomic on a per-worker basis. Once an +// extension configuration is applied to a request or a connection, it remains +// constant for the duration of processing. If the initial delivery of the +// extension configuration fails, due to a timeout for example, the optional +// default configuration is applied. Without a default configuration, the +// extension is disabled, until an extension configuration is received. The +// behavior of a disabled extension depends on the context. For example, a +// filter chain with a disabled extension filter rejects all incoming streams. +message ExtensionConfigSource { + ConfigSource config_source = 1 [(validate.rules).any = {required: true}]; + + // Optional default configuration to use as the initial configuration if + // there is a failure to receive the initial extension configuration or if + // `apply_default_config_without_warming` flag is set. + google.protobuf.Any default_config = 2; + + // Use the default config as the initial configuration without warming and + // waiting for the first discovery response. Requires the default configuration + // to be supplied. + bool apply_default_config_without_warming = 3; + + // A set of permitted extension type URLs. Extension configuration updates are rejected + // if they do not match any type URL in the set. + repeated string type_urls = 4 [(validate.rules).repeated = {min_items: 1}]; +} diff --git a/api/envoy/config/core/v4alpha/extension.proto b/api/envoy/config/core/v4alpha/extension.proto index 52ae2a143b49..4de107580d07 100644 --- a/api/envoy/config/core/v4alpha/extension.proto +++ b/api/envoy/config/core/v4alpha/extension.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.config.core.v4alpha; +import "envoy/config/core/v4alpha/config_source.proto"; + import "google/protobuf/any.proto"; import "udpa/annotations/status.proto"; @@ -32,3 +34,35 @@ message TypedExtensionConfig { // ` for further details. google.protobuf.Any typed_config = 2 [(validate.rules).any = {required: true}]; } + +// Configuration source specifier for a late-bound extension configuration. The +// parent resource is warmed until all the initial extension configurations are +// received, unless the flag to apply the default configuration is set. +// Subsequent extension updates are atomic on a per-worker basis. Once an +// extension configuration is applied to a request or a connection, it remains +// constant for the duration of processing. If the initial delivery of the +// extension configuration fails, due to a timeout for example, the optional +// default configuration is applied. Without a default configuration, the +// extension is disabled, until an extension configuration is received. The +// behavior of a disabled extension depends on the context. For example, a +// filter chain with a disabled extension filter rejects all incoming streams. +message ExtensionConfigSource { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.core.v3.ExtensionConfigSource"; + + ConfigSource config_source = 1 [(validate.rules).any = {required: true}]; + + // Optional default configuration to use as the initial configuration if + // there is a failure to receive the initial extension configuration or if + // `apply_default_config_without_warming` flag is set. + google.protobuf.Any default_config = 2; + + // Use the default config as the initial configuration without warming and + // waiting for the first discovery response. Requires the default configuration + // to be supplied. + bool apply_default_config_without_warming = 3; + + // A set of permitted extension type URLs. Extension configuration updates are rejected + // if they do not match any type URL in the set. + repeated string type_urls = 4 [(validate.rules).repeated = {min_items: 1}]; +} diff --git a/api/envoy/data/accesslog/v3/accesslog.proto b/api/envoy/data/accesslog/v3/accesslog.proto index 347adc2003e6..c16b5be1ff0e 100644 --- a/api/envoy/data/accesslog/v3/accesslog.proto +++ b/api/envoy/data/accesslog/v3/accesslog.proto @@ -186,7 +186,7 @@ message AccessLogCommon { } // Flags indicating occurrences during request/response processing. -// [#next-free-field: 22] +// [#next-free-field: 23] message ResponseFlags { option (udpa.annotations.versioning).previous_message_type = "envoy.data.accesslog.v2.ResponseFlags"; @@ -269,6 +269,9 @@ message ResponseFlags { // Indicates the response was served from a cache filter. bool response_from_cache_filter = 21; + + // Indicates that a filter configuration is not available. + bool no_filter_config_found = 22; } // Properties of a negotiated TLS connection. diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 87e629f4f441..04a132ad2672 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -5,6 +5,7 @@ package envoy.extensions.filters.network.http_connection_manager.v3; import "envoy/config/accesslog/v3/accesslog.proto"; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/config_source.proto"; +import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/protocol.proto"; import "envoy/config/core/v3/substitution_format_string.proto"; import "envoy/config/route/v3/route.proto"; @@ -797,38 +798,13 @@ message HttpFilter { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpFilter"; - // [#not-implemented-hide:] Configuration source specifier for the late-bound - // filter configuration. The HTTP Listener is warmed until all the initial - // filter configurations are received, unless the flag to apply the default - // configuration is set. Subsequent filter updates are atomic on a per-worker - // basis, and apply to new streams while the active streams continue using - // the older filter configurations. If the initial delivery of the filter - // configuration fails, due to a timeout for example, the optional default - // configuration is applied. Without a default configuration, the filter is - // disabled, and the HTTP listener responds with 500 immediately. After the - // failure, the listener continues subscribing to the subsequent filter - // configurations. - message HttpFilterConfigSource { - config.core.v3.ConfigSource config_source = 1; - - // Optional default configuration to use as the initial configuration if - // there is a failure to receive the initial filter configuration or if - // `apply_default_config_without_warming` flag is set. - google.protobuf.Any default_config = 2; - - // Use the default config as the initial configuration without warming and - // waiting for the first xDS response. Requires the default configuration - // to be supplied. - bool apply_default_config_without_warming = 3; - } - reserved 3, 2; reserved "config"; // The name of the filter configuration. The name is used as a fallback to // select an extension if the type of the configuration proto is not - // sufficient. It also serves as a resource name in FilterConfigDS. + // sufficient. It also serves as a resource name in ExtensionConfigDS. string name = 1 [(validate.rules).string = {min_bytes: 1}]; // Filter specific configuration which depends on the filter being instantiated. See the supported @@ -836,8 +812,10 @@ message HttpFilter { oneof config_type { google.protobuf.Any typed_config = 4; - // [#not-implemented-hide:] Configuration source specifier for FilterConfigDS. - HttpFilterConfigSource filter_config_ds = 5; + // Configuration source specifier for an extension configuration discovery service. + // In case of a failure and without the default configuration, the HTTP listener responds with 500. + // Extension configs delivered through this mechanism are not expected to require warming (see https://github.com/envoyproxy/envoy/issues/12061). + config.core.v3.ExtensionConfigSource config_discovery = 5; } } diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index ac31bf1ecd62..042a39863f81 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -5,6 +5,7 @@ package envoy.extensions.filters.network.http_connection_manager.v4alpha; import "envoy/config/accesslog/v4alpha/accesslog.proto"; import "envoy/config/core/v4alpha/base.proto"; import "envoy/config/core/v4alpha/config_source.proto"; +import "envoy/config/core/v4alpha/extension.proto"; import "envoy/config/core/v4alpha/protocol.proto"; import "envoy/config/core/v4alpha/substitution_format_string.proto"; import "envoy/config/route/v4alpha/route.proto"; @@ -803,42 +804,13 @@ message HttpFilter { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter"; - // [#not-implemented-hide:] Configuration source specifier for the late-bound - // filter configuration. The HTTP Listener is warmed until all the initial - // filter configurations are received, unless the flag to apply the default - // configuration is set. Subsequent filter updates are atomic on a per-worker - // basis, and apply to new streams while the active streams continue using - // the older filter configurations. If the initial delivery of the filter - // configuration fails, due to a timeout for example, the optional default - // configuration is applied. Without a default configuration, the filter is - // disabled, and the HTTP listener responds with 500 immediately. After the - // failure, the listener continues subscribing to the subsequent filter - // configurations. - message HttpFilterConfigSource { - option (udpa.annotations.versioning).previous_message_type = - "envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter." - "HttpFilterConfigSource"; - - config.core.v4alpha.ConfigSource config_source = 1; - - // Optional default configuration to use as the initial configuration if - // there is a failure to receive the initial filter configuration or if - // `apply_default_config_without_warming` flag is set. - google.protobuf.Any default_config = 2; - - // Use the default config as the initial configuration without warming and - // waiting for the first xDS response. Requires the default configuration - // to be supplied. - bool apply_default_config_without_warming = 3; - } - reserved 3, 2; reserved "config"; // The name of the filter configuration. The name is used as a fallback to // select an extension if the type of the configuration proto is not - // sufficient. It also serves as a resource name in FilterConfigDS. + // sufficient. It also serves as a resource name in ExtensionConfigDS. string name = 1 [(validate.rules).string = {min_bytes: 1}]; // Filter specific configuration which depends on the filter being instantiated. See the supported @@ -846,8 +818,10 @@ message HttpFilter { oneof config_type { google.protobuf.Any typed_config = 4; - // [#not-implemented-hide:] Configuration source specifier for FilterConfigDS. - HttpFilterConfigSource filter_config_ds = 5; + // Configuration source specifier for an extension configuration discovery service. + // In case of a failure and without the default configuration, the HTTP listener responds with 500. + // Extension configs delivered through this mechanism are not expected to require warming (see https://github.com/envoyproxy/envoy/issues/12061). + config.core.v4alpha.ExtensionConfigSource config_discovery = 5; } } diff --git a/api/envoy/service/filter/v3/BUILD b/api/envoy/service/extension/v3/BUILD similarity index 100% rename from api/envoy/service/filter/v3/BUILD rename to api/envoy/service/extension/v3/BUILD diff --git a/api/envoy/service/filter/v3/filter_config_discovery.proto b/api/envoy/service/extension/v3/config_discovery.proto similarity index 52% rename from api/envoy/service/filter/v3/filter_config_discovery.proto rename to api/envoy/service/extension/v3/config_discovery.proto index 79c5846710bb..ce2a5c7dfe70 100644 --- a/api/envoy/service/filter/v3/filter_config_discovery.proto +++ b/api/envoy/service/extension/v3/config_discovery.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package envoy.service.filter.v3; +package envoy.service.extension.v3; import "envoy/service/discovery/v3/discovery.proto"; @@ -10,28 +10,29 @@ import "envoy/annotations/resource.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; -option java_package = "io.envoyproxy.envoy.service.filter.v3"; -option java_outer_classname = "FilterConfigDiscoveryProto"; +option java_package = "io.envoyproxy.envoy.service.extension.v3"; +option java_outer_classname = "ConfigDiscoveryProto"; option java_multiple_files = true; option java_generic_services = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [#protodoc-title: FilterConfigDS] +// [#protodoc-title: ExtensionConfigDS] -// Return filter configurations. -service FilterConfigDiscoveryService { +// Return extension configurations. +service ExtensionConfigDiscoveryService { option (envoy.annotations.resource).type = "envoy.config.core.v3.TypedExtensionConfig"; - rpc StreamFilterConfigs(stream discovery.v3.DiscoveryRequest) + rpc StreamExtensionConfigs(stream discovery.v3.DiscoveryRequest) returns (stream discovery.v3.DiscoveryResponse) { } - rpc DeltaFilterConfigs(stream discovery.v3.DeltaDiscoveryRequest) + rpc DeltaExtensionConfigs(stream discovery.v3.DeltaDiscoveryRequest) returns (stream discovery.v3.DeltaDiscoveryResponse) { } - rpc FetchFilterConfigs(discovery.v3.DiscoveryRequest) returns (discovery.v3.DiscoveryResponse) { - option (google.api.http).post = "/v3/discovery:filter_configs"; + rpc FetchExtensionConfigs(discovery.v3.DiscoveryRequest) + returns (discovery.v3.DiscoveryResponse) { + option (google.api.http).post = "/v3/discovery:extension_configs"; option (google.api.http).body = "*"; } } diff --git a/api/versioning/BUILD b/api/versioning/BUILD index e00a0fbbb55d..00939e940295 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -128,7 +128,7 @@ proto_library( "//envoy/service/discovery/v3:pkg", "//envoy/service/endpoint/v3:pkg", "//envoy/service/event_reporting/v3:pkg", - "//envoy/service/filter/v3:pkg", + "//envoy/service/extension/v3:pkg", "//envoy/service/health/v3:pkg", "//envoy/service/listener/v3:pkg", "//envoy/service/load_stats/v3:pkg", diff --git a/docs/root/api-v3/service/service.rst b/docs/root/api-v3/service/service.rst index de8110cf5fbd..d651856c678b 100644 --- a/docs/root/api-v3/service/service.rst +++ b/docs/root/api-v3/service/service.rst @@ -16,3 +16,4 @@ Services tap/v3/* ../config/tap/v3/* trace/v3/* + extension/v3/* diff --git a/docs/root/configuration/overview/extension.rst b/docs/root/configuration/overview/extension.rst index 37f58b8ecad7..dab59eaf6b97 100644 --- a/docs/root/configuration/overview/extension.rst +++ b/docs/root/configuration/overview/extension.rst @@ -61,3 +61,25 @@ follows: "@type": type.googleapis.com/udpa.type.v1.TypedStruct type_url: type.googleapis.com/envoy.extensions.filters.http.router.v3Router +Discovery service +^^^^^^^^^^^^^^^^^ + +Extension configuration can be supplied dynamically from a :ref:`an xDS +management server` using :ref:`ExtensionConfiguration discovery +service`. +The name field in the extension configuration acts as the resource identifier. +For example, HTTP connection manager supports :ref:`dynamic filter +re-configuration` +for HTTP filters. + +Extension config discovery service has a :ref:`statistics +` tree rooted at +*.extension_config_discovery..*. In addition +to the common subscription statistics, it also provides the following: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + config_reload, Counter, Total number of successful configuration updates + config_fail, Counter, Total number of failed configuration updates diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index e2fcea52f2c2..a768a80008b8 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -52,6 +52,8 @@ New Features that track headers and body sizes of requests and responses. * stats: allow configuring histogram buckets for stats sinks and admin endpoints that support it. * tap: added :ref:`generic body matcher` to scan http requests and responses for text or hex patterns. +* tcp: switched the TCP connection pool to the new "shared" connection pool, sharing a common code base with HTTP and HTTP/2. Any unexpected behavioral changes can be temporarily reverted by setting `envoy.reloadable_features.new_tcp_connection_pool` to false. +* xds: added :ref:`extension config discovery` support for HTTP filters. Deprecated ---------- diff --git a/generated_api_shadow/envoy/config/accesslog/v3/accesslog.proto b/generated_api_shadow/envoy/config/accesslog/v3/accesslog.proto index 09d691dd3665..3307b4c57ffd 100644 --- a/generated_api_shadow/envoy/config/accesslog/v3/accesslog.proto +++ b/generated_api_shadow/envoy/config/accesslog/v3/accesslog.proto @@ -240,6 +240,7 @@ message ResponseFlagFilter { in: "DPE" in: "UMSDR" in: "RFCF" + in: "NFCF" } } }]; diff --git a/generated_api_shadow/envoy/config/accesslog/v4alpha/accesslog.proto b/generated_api_shadow/envoy/config/accesslog/v4alpha/accesslog.proto index 939d4df95889..35f494ea1ac8 100644 --- a/generated_api_shadow/envoy/config/accesslog/v4alpha/accesslog.proto +++ b/generated_api_shadow/envoy/config/accesslog/v4alpha/accesslog.proto @@ -241,6 +241,7 @@ message ResponseFlagFilter { in: "DPE" in: "UMSDR" in: "RFCF" + in: "NFCF" } } }]; diff --git a/generated_api_shadow/envoy/config/core/v3/extension.proto b/generated_api_shadow/envoy/config/core/v3/extension.proto index 636398760785..ba66da6a8e36 100644 --- a/generated_api_shadow/envoy/config/core/v3/extension.proto +++ b/generated_api_shadow/envoy/config/core/v3/extension.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.config.core.v3; +import "envoy/config/core/v3/config_source.proto"; + import "google/protobuf/any.proto"; import "udpa/annotations/status.proto"; @@ -28,3 +30,32 @@ message TypedExtensionConfig { // ` for further details. google.protobuf.Any typed_config = 2 [(validate.rules).any = {required: true}]; } + +// Configuration source specifier for a late-bound extension configuration. The +// parent resource is warmed until all the initial extension configurations are +// received, unless the flag to apply the default configuration is set. +// Subsequent extension updates are atomic on a per-worker basis. Once an +// extension configuration is applied to a request or a connection, it remains +// constant for the duration of processing. If the initial delivery of the +// extension configuration fails, due to a timeout for example, the optional +// default configuration is applied. Without a default configuration, the +// extension is disabled, until an extension configuration is received. The +// behavior of a disabled extension depends on the context. For example, a +// filter chain with a disabled extension filter rejects all incoming streams. +message ExtensionConfigSource { + ConfigSource config_source = 1 [(validate.rules).any = {required: true}]; + + // Optional default configuration to use as the initial configuration if + // there is a failure to receive the initial extension configuration or if + // `apply_default_config_without_warming` flag is set. + google.protobuf.Any default_config = 2; + + // Use the default config as the initial configuration without warming and + // waiting for the first discovery response. Requires the default configuration + // to be supplied. + bool apply_default_config_without_warming = 3; + + // A set of permitted extension type URLs. Extension configuration updates are rejected + // if they do not match any type URL in the set. + repeated string type_urls = 4 [(validate.rules).repeated = {min_items: 1}]; +} diff --git a/generated_api_shadow/envoy/config/core/v4alpha/extension.proto b/generated_api_shadow/envoy/config/core/v4alpha/extension.proto index 52ae2a143b49..4de107580d07 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/extension.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/extension.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.config.core.v4alpha; +import "envoy/config/core/v4alpha/config_source.proto"; + import "google/protobuf/any.proto"; import "udpa/annotations/status.proto"; @@ -32,3 +34,35 @@ message TypedExtensionConfig { // ` for further details. google.protobuf.Any typed_config = 2 [(validate.rules).any = {required: true}]; } + +// Configuration source specifier for a late-bound extension configuration. The +// parent resource is warmed until all the initial extension configurations are +// received, unless the flag to apply the default configuration is set. +// Subsequent extension updates are atomic on a per-worker basis. Once an +// extension configuration is applied to a request or a connection, it remains +// constant for the duration of processing. If the initial delivery of the +// extension configuration fails, due to a timeout for example, the optional +// default configuration is applied. Without a default configuration, the +// extension is disabled, until an extension configuration is received. The +// behavior of a disabled extension depends on the context. For example, a +// filter chain with a disabled extension filter rejects all incoming streams. +message ExtensionConfigSource { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.core.v3.ExtensionConfigSource"; + + ConfigSource config_source = 1 [(validate.rules).any = {required: true}]; + + // Optional default configuration to use as the initial configuration if + // there is a failure to receive the initial extension configuration or if + // `apply_default_config_without_warming` flag is set. + google.protobuf.Any default_config = 2; + + // Use the default config as the initial configuration without warming and + // waiting for the first discovery response. Requires the default configuration + // to be supplied. + bool apply_default_config_without_warming = 3; + + // A set of permitted extension type URLs. Extension configuration updates are rejected + // if they do not match any type URL in the set. + repeated string type_urls = 4 [(validate.rules).repeated = {min_items: 1}]; +} diff --git a/generated_api_shadow/envoy/data/accesslog/v3/accesslog.proto b/generated_api_shadow/envoy/data/accesslog/v3/accesslog.proto index 347adc2003e6..c16b5be1ff0e 100644 --- a/generated_api_shadow/envoy/data/accesslog/v3/accesslog.proto +++ b/generated_api_shadow/envoy/data/accesslog/v3/accesslog.proto @@ -186,7 +186,7 @@ message AccessLogCommon { } // Flags indicating occurrences during request/response processing. -// [#next-free-field: 22] +// [#next-free-field: 23] message ResponseFlags { option (udpa.annotations.versioning).previous_message_type = "envoy.data.accesslog.v2.ResponseFlags"; @@ -269,6 +269,9 @@ message ResponseFlags { // Indicates the response was served from a cache filter. bool response_from_cache_filter = 21; + + // Indicates that a filter configuration is not available. + bool no_filter_config_found = 22; } // Properties of a negotiated TLS connection. diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index a25759c85fc7..0439633d6e6e 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -5,6 +5,7 @@ package envoy.extensions.filters.network.http_connection_manager.v3; import "envoy/config/accesslog/v3/accesslog.proto"; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/config_source.proto"; +import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/protocol.proto"; import "envoy/config/core/v3/substitution_format_string.proto"; import "envoy/config/route/v3/route.proto"; @@ -802,36 +803,11 @@ message HttpFilter { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpFilter"; - // [#not-implemented-hide:] Configuration source specifier for the late-bound - // filter configuration. The HTTP Listener is warmed until all the initial - // filter configurations are received, unless the flag to apply the default - // configuration is set. Subsequent filter updates are atomic on a per-worker - // basis, and apply to new streams while the active streams continue using - // the older filter configurations. If the initial delivery of the filter - // configuration fails, due to a timeout for example, the optional default - // configuration is applied. Without a default configuration, the filter is - // disabled, and the HTTP listener responds with 500 immediately. After the - // failure, the listener continues subscribing to the subsequent filter - // configurations. - message HttpFilterConfigSource { - config.core.v3.ConfigSource config_source = 1; - - // Optional default configuration to use as the initial configuration if - // there is a failure to receive the initial filter configuration or if - // `apply_default_config_without_warming` flag is set. - google.protobuf.Any default_config = 2; - - // Use the default config as the initial configuration without warming and - // waiting for the first xDS response. Requires the default configuration - // to be supplied. - bool apply_default_config_without_warming = 3; - } - reserved 3; // The name of the filter configuration. The name is used as a fallback to // select an extension if the type of the configuration proto is not - // sufficient. It also serves as a resource name in FilterConfigDS. + // sufficient. It also serves as a resource name in ExtensionConfigDS. string name = 1 [(validate.rules).string = {min_bytes: 1}]; // Filter specific configuration which depends on the filter being instantiated. See the supported @@ -839,8 +815,10 @@ message HttpFilter { oneof config_type { google.protobuf.Any typed_config = 4; - // [#not-implemented-hide:] Configuration source specifier for FilterConfigDS. - HttpFilterConfigSource filter_config_ds = 5; + // Configuration source specifier for an extension configuration discovery service. + // In case of a failure and without the default configuration, the HTTP listener responds with 500. + // Extension configs delivered through this mechanism are not expected to require warming (see https://github.com/envoyproxy/envoy/issues/12061). + config.core.v3.ExtensionConfigSource config_discovery = 5; google.protobuf.Struct hidden_envoy_deprecated_config = 2 [deprecated = true]; } diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index ac31bf1ecd62..042a39863f81 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -5,6 +5,7 @@ package envoy.extensions.filters.network.http_connection_manager.v4alpha; import "envoy/config/accesslog/v4alpha/accesslog.proto"; import "envoy/config/core/v4alpha/base.proto"; import "envoy/config/core/v4alpha/config_source.proto"; +import "envoy/config/core/v4alpha/extension.proto"; import "envoy/config/core/v4alpha/protocol.proto"; import "envoy/config/core/v4alpha/substitution_format_string.proto"; import "envoy/config/route/v4alpha/route.proto"; @@ -803,42 +804,13 @@ message HttpFilter { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter"; - // [#not-implemented-hide:] Configuration source specifier for the late-bound - // filter configuration. The HTTP Listener is warmed until all the initial - // filter configurations are received, unless the flag to apply the default - // configuration is set. Subsequent filter updates are atomic on a per-worker - // basis, and apply to new streams while the active streams continue using - // the older filter configurations. If the initial delivery of the filter - // configuration fails, due to a timeout for example, the optional default - // configuration is applied. Without a default configuration, the filter is - // disabled, and the HTTP listener responds with 500 immediately. After the - // failure, the listener continues subscribing to the subsequent filter - // configurations. - message HttpFilterConfigSource { - option (udpa.annotations.versioning).previous_message_type = - "envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter." - "HttpFilterConfigSource"; - - config.core.v4alpha.ConfigSource config_source = 1; - - // Optional default configuration to use as the initial configuration if - // there is a failure to receive the initial filter configuration or if - // `apply_default_config_without_warming` flag is set. - google.protobuf.Any default_config = 2; - - // Use the default config as the initial configuration without warming and - // waiting for the first xDS response. Requires the default configuration - // to be supplied. - bool apply_default_config_without_warming = 3; - } - reserved 3, 2; reserved "config"; // The name of the filter configuration. The name is used as a fallback to // select an extension if the type of the configuration proto is not - // sufficient. It also serves as a resource name in FilterConfigDS. + // sufficient. It also serves as a resource name in ExtensionConfigDS. string name = 1 [(validate.rules).string = {min_bytes: 1}]; // Filter specific configuration which depends on the filter being instantiated. See the supported @@ -846,8 +818,10 @@ message HttpFilter { oneof config_type { google.protobuf.Any typed_config = 4; - // [#not-implemented-hide:] Configuration source specifier for FilterConfigDS. - HttpFilterConfigSource filter_config_ds = 5; + // Configuration source specifier for an extension configuration discovery service. + // In case of a failure and without the default configuration, the HTTP listener responds with 500. + // Extension configs delivered through this mechanism are not expected to require warming (see https://github.com/envoyproxy/envoy/issues/12061). + config.core.v4alpha.ExtensionConfigSource config_discovery = 5; } } diff --git a/generated_api_shadow/envoy/service/filter/v3/BUILD b/generated_api_shadow/envoy/service/extension/v3/BUILD similarity index 100% rename from generated_api_shadow/envoy/service/filter/v3/BUILD rename to generated_api_shadow/envoy/service/extension/v3/BUILD diff --git a/generated_api_shadow/envoy/service/filter/v3/filter_config_discovery.proto b/generated_api_shadow/envoy/service/extension/v3/config_discovery.proto similarity index 52% rename from generated_api_shadow/envoy/service/filter/v3/filter_config_discovery.proto rename to generated_api_shadow/envoy/service/extension/v3/config_discovery.proto index 79c5846710bb..ce2a5c7dfe70 100644 --- a/generated_api_shadow/envoy/service/filter/v3/filter_config_discovery.proto +++ b/generated_api_shadow/envoy/service/extension/v3/config_discovery.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package envoy.service.filter.v3; +package envoy.service.extension.v3; import "envoy/service/discovery/v3/discovery.proto"; @@ -10,28 +10,29 @@ import "envoy/annotations/resource.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; -option java_package = "io.envoyproxy.envoy.service.filter.v3"; -option java_outer_classname = "FilterConfigDiscoveryProto"; +option java_package = "io.envoyproxy.envoy.service.extension.v3"; +option java_outer_classname = "ConfigDiscoveryProto"; option java_multiple_files = true; option java_generic_services = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [#protodoc-title: FilterConfigDS] +// [#protodoc-title: ExtensionConfigDS] -// Return filter configurations. -service FilterConfigDiscoveryService { +// Return extension configurations. +service ExtensionConfigDiscoveryService { option (envoy.annotations.resource).type = "envoy.config.core.v3.TypedExtensionConfig"; - rpc StreamFilterConfigs(stream discovery.v3.DiscoveryRequest) + rpc StreamExtensionConfigs(stream discovery.v3.DiscoveryRequest) returns (stream discovery.v3.DiscoveryResponse) { } - rpc DeltaFilterConfigs(stream discovery.v3.DeltaDiscoveryRequest) + rpc DeltaExtensionConfigs(stream discovery.v3.DeltaDiscoveryRequest) returns (stream discovery.v3.DeltaDiscoveryResponse) { } - rpc FetchFilterConfigs(discovery.v3.DiscoveryRequest) returns (discovery.v3.DiscoveryResponse) { - option (google.api.http).post = "/v3/discovery:filter_configs"; + rpc FetchExtensionConfigs(discovery.v3.DiscoveryRequest) + returns (discovery.v3.DiscoveryResponse) { + option (google.api.http).post = "/v3/discovery:extension_configs"; option (google.api.http).body = "*"; } } diff --git a/include/envoy/config/BUILD b/include/envoy/config/BUILD index 67ac833c2403..96140621aa6b 100644 --- a/include/envoy/config/BUILD +++ b/include/envoy/config/BUILD @@ -29,6 +29,12 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "extension_config_provider_interface", + hdrs = ["extension_config_provider.h"], + deps = ["//source/common/protobuf"], +) + envoy_cc_library( name = "grpc_mux_interface", hdrs = ["grpc_mux.h"], diff --git a/include/envoy/config/extension_config_provider.h b/include/envoy/config/extension_config_provider.h new file mode 100644 index 000000000000..0ea1aef9adc3 --- /dev/null +++ b/include/envoy/config/extension_config_provider.h @@ -0,0 +1,53 @@ +#pragma once + +#include "envoy/common/pure.h" + +#include "common/protobuf/protobuf.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Config { + +/** + * A provider for extension configurations obtained either statically or via + * the extension configuration discovery service. Dynamically updated extension + * configurations may share subscriptions across extension config providers. + */ +template class ExtensionConfigProvider { +public: + virtual ~ExtensionConfigProvider() = default; + + /** + * Get the extension configuration resource name. + **/ + virtual const std::string& name() PURE; + + /** + * @return FactoryCallback an extension factory callback. Note that if the + * provider has not yet performed an initial configuration load and no + * default is provided, an empty optional will be returned. The factory + * callback is the latest version of the extension configuration, and should + * generally apply only to new requests and connections. + */ + virtual absl::optional config() PURE; + + /** + * Validate that the configuration is applicable in the context of the provider. If an exception + * is thrown by any of the config providers for an update, the extension configuration update is + * rejected. + * @param proto_config is the candidate configuration update. + * @param factory used to instantiate an extension config. + */ + virtual void validateConfig(const ProtobufWkt::Any& proto_config, Factory& factory) PURE; + + /** + * Update the provider with a new configuration. + * @param config is an extension factory callback to replace the existing configuration. + * @param version_info is the version of the new extension configuration. + */ + virtual void onConfigUpdate(FactoryCallback config, const std::string& version_info) PURE; +}; + +} // namespace Config +} // namespace Envoy diff --git a/include/envoy/filter/http/BUILD b/include/envoy/filter/http/BUILD new file mode 100644 index 000000000000..5a76c4ba7b9d --- /dev/null +++ b/include/envoy/filter/http/BUILD @@ -0,0 +1,21 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "filter_config_provider_interface", + hdrs = ["filter_config_provider.h"], + deps = [ + "//include/envoy/config:extension_config_provider_interface", + "//include/envoy/http:filter_interface", + "//include/envoy/init:manager_interface", + "//include/envoy/server:filter_config_interface", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) diff --git a/include/envoy/filter/http/filter_config_provider.h b/include/envoy/filter/http/filter_config_provider.h new file mode 100644 index 000000000000..e1c3f58c125a --- /dev/null +++ b/include/envoy/filter/http/filter_config_provider.h @@ -0,0 +1,57 @@ +#pragma once + +#include "envoy/config/core/v3/config_source.pb.h" +#include "envoy/config/extension_config_provider.h" +#include "envoy/http/filter.h" +#include "envoy/init/manager.h" +#include "envoy/server/filter_config.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Filter { +namespace Http { + +using FilterConfigProvider = + Envoy::Config::ExtensionConfigProvider; +using FilterConfigProviderPtr = std::unique_ptr; + +/** + * The FilterConfigProviderManager exposes the ability to get an FilterConfigProvider + * for both static and dynamic filter config providers. + */ +class FilterConfigProviderManager { +public: + virtual ~FilterConfigProviderManager() = default; + + /** + * Get an FilterConfigProviderPtr for a filter config. The config providers may share + * the underlying subscriptions to the filter config discovery service. + * @param config_source supplies the configuration source for the filter configs. + * @param filter_config_name the filter config resource name. + * @param require_type_urls enforces that the typed filter config must have a certain type URL. + * @param factory_context is the context to use for the filter config provider. + * @param stat_prefix supplies the stat_prefix to use for the provider stats. + * @param apply_without_warming initializes immediately with the default config and starts the + * subscription. + */ + virtual FilterConfigProviderPtr createDynamicFilterConfigProvider( + const envoy::config::core::v3::ConfigSource& config_source, + const std::string& filter_config_name, const std::set& require_type_urls, + Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix, + bool apply_without_warming) PURE; + + /** + * Get an FilterConfigProviderPtr for a statically inlined filter config. + * @param config is a fully resolved filter instantiation factory. + * @param filter_config_name is the name of the filter configuration resource. + */ + virtual FilterConfigProviderPtr + createStaticFilterConfigProvider(const Envoy::Http::FilterFactoryCb& config, + const std::string& filter_config_name) PURE; +}; + +} // namespace Http +} // namespace Filter +} // namespace Envoy diff --git a/include/envoy/stream_info/stream_info.h b/include/envoy/stream_info/stream_info.h index fbec2554d380..515d4e83c744 100644 --- a/include/envoy/stream_info/stream_info.h +++ b/include/envoy/stream_info/stream_info.h @@ -76,8 +76,10 @@ enum ResponseFlag { UpstreamMaxStreamDurationReached = 0x80000, // True if the response was served from an Envoy cache filter. ResponseFromCacheFilter = 0x100000, + // Filter config was not received within the permitted warming deadline. + NoFilterConfigFound = 0x200000, // ATTENTION: MAKE SURE THIS REMAINS EQUAL TO THE LAST FLAG. - LastFlag = ResponseFromCacheFilter + LastFlag = NoFilterConfigFound }; /** diff --git a/source/common/config/BUILD b/source/common/config/BUILD index e42e10d7803b..b93d24af1fbf 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -253,6 +253,7 @@ envoy_cc_library( "@envoy_api//envoy/service/discovery/v2:pkg_cc_proto", "@envoy_api//envoy/service/discovery/v3:pkg_cc_proto", "@envoy_api//envoy/service/endpoint/v3:pkg_cc_proto", + "@envoy_api//envoy/service/extension/v3:pkg_cc_proto", "@envoy_api//envoy/service/listener/v3:pkg_cc_proto", "@envoy_api//envoy/service/ratelimit/v2:pkg_cc_proto", "@envoy_api//envoy/service/ratelimit/v3:pkg_cc_proto", diff --git a/source/common/config/protobuf_link_hacks.h b/source/common/config/protobuf_link_hacks.h index efcfa08f0c23..b613d60ff84c 100644 --- a/source/common/config/protobuf_link_hacks.h +++ b/source/common/config/protobuf_link_hacks.h @@ -13,6 +13,7 @@ #include "envoy/service/discovery/v2/sds.pb.h" #include "envoy/service/discovery/v3/ads.pb.h" #include "envoy/service/endpoint/v3/eds.pb.h" +#include "envoy/service/extension/v3/config_discovery.pb.h" #include "envoy/service/listener/v3/lds.pb.h" #include "envoy/service/ratelimit/v2/rls.pb.h" #include "envoy/service/ratelimit/v3/rls.pb.h" diff --git a/source/common/config/type_to_endpoint.cc b/source/common/config/type_to_endpoint.cc index d7434aaa01f8..9821b288dcbc 100644 --- a/source/common/config/type_to_endpoint.cc +++ b/source/common/config/type_to_endpoint.cc @@ -177,6 +177,13 @@ TypeUrlToVersionedServiceMap* buildTypeUrlToServiceMap() { "envoy.service.listener.v3.ListenerDiscoveryService"), SERVICE_VERSION_INFO("envoy.service.discovery.v2.RuntimeDiscoveryService", "envoy.service.runtime.v3.RuntimeDiscoveryService"), + ServiceVersionInfoMap{{ + "envoy.service.extension.v3.ExtensionConfigDiscoveryService", + ServiceVersionInfo{{ + {envoy::config::core::v3::ApiVersion::V3, + "envoy.service.extension.v3.ExtensionConfigDiscoveryService"}, + }}, + }}, }) { for (const auto& registered_service : registered) { const TypeUrl resource_type_url = getResourceTypeUrl(registered_service.first); diff --git a/source/common/config/utility.h b/source/common/config/utility.h index 09b9e0ea6f37..b4ee90445adf 100644 --- a/source/common/config/utility.h +++ b/source/common/config/utility.h @@ -236,27 +236,42 @@ class Utility { */ template static Factory& getAndCheckFactory(const ProtoMessage& message) { - const ProtobufWkt::Any& typed_config = message.typed_config(); + Factory* factory = Utility::getFactoryByType(message.typed_config()); + if (factory != nullptr) { + return *factory; + } + + return Utility::getAndCheckFactoryByName(message.name()); + } + + /** + * Get type URL from a typed config. + * @param typed_config for the extension config. + */ + static std::string getFactoryType(const ProtobufWkt::Any& typed_config) { static const std::string& typed_struct_type = udpa::type::v1::TypedStruct::default_instance().GetDescriptor()->full_name(); - - if (!typed_config.type_url().empty()) { - // Unpack methods will only use the fully qualified type name after the last '/'. - // https://github.com/protocolbuffers/protobuf/blob/3.6.x/src/google/protobuf/any.proto#L87 - auto type = std::string(TypeUtil::typeUrlToDescriptorFullName(typed_config.type_url())); - if (type == typed_struct_type) { - udpa::type::v1::TypedStruct typed_struct; - MessageUtil::unpackTo(typed_config, typed_struct); - // Not handling nested structs or typed structs in typed structs - type = std::string(TypeUtil::typeUrlToDescriptorFullName(typed_struct.type_url())); - } - Factory* factory = Registry::FactoryRegistry::getFactoryByType(type); - if (factory != nullptr) { - return *factory; - } + // Unpack methods will only use the fully qualified type name after the last '/'. + // https://github.com/protocolbuffers/protobuf/blob/3.6.x/src/google/protobuf/any.proto#L87 + auto type = std::string(TypeUtil::typeUrlToDescriptorFullName(typed_config.type_url())); + if (type == typed_struct_type) { + udpa::type::v1::TypedStruct typed_struct; + MessageUtil::unpackTo(typed_config, typed_struct); + // Not handling nested structs or typed structs in typed structs + return std::string(TypeUtil::typeUrlToDescriptorFullName(typed_struct.type_url())); } + return type; + } - return Utility::getAndCheckFactoryByName(message.name()); + /** + * Get a Factory from the registry by type URL. + * @param typed_config for the extension config. + */ + template static Factory* getFactoryByType(const ProtobufWkt::Any& typed_config) { + if (typed_config.type_url().empty()) { + return nullptr; + } + return Registry::FactoryRegistry::getFactoryByType(getFactoryType(typed_config)); } /** diff --git a/source/common/filter/http/BUILD b/source/common/filter/http/BUILD new file mode 100644 index 000000000000..888c2fd44b12 --- /dev/null +++ b/source/common/filter/http/BUILD @@ -0,0 +1,31 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "filter_config_discovery_lib", + srcs = ["filter_config_discovery_impl.cc"], + hdrs = ["filter_config_discovery_impl.h"], + deps = [ + "//include/envoy/config:subscription_interface", + "//include/envoy/filter/http:filter_config_provider_interface", + "//include/envoy/singleton:instance_interface", + "//include/envoy/stats:stats_macros", + "//include/envoy/thread_local:thread_local_interface", + "//source/common/config:subscription_base_interface", + "//source/common/config:subscription_factory_lib", + "//source/common/config:utility_lib", + "//source/common/grpc:common_lib", + "//source/common/init:manager_lib", + "//source/common/init:target_lib", + "//source/common/init:watcher_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) diff --git a/source/common/filter/http/filter_config_discovery_impl.cc b/source/common/filter/http/filter_config_discovery_impl.cc new file mode 100644 index 000000000000..eccab3fe988d --- /dev/null +++ b/source/common/filter/http/filter_config_discovery_impl.cc @@ -0,0 +1,211 @@ +#include "common/filter/http/filter_config_discovery_impl.h" + +#include "envoy/config/core/v3/extension.pb.validate.h" +#include "envoy/server/filter_config.h" + +#include "common/config/utility.h" +#include "common/grpc/common.h" +#include "common/protobuf/utility.h" + +#include "absl/strings/str_join.h" + +namespace Envoy { +namespace Filter { +namespace Http { + +DynamicFilterConfigProviderImpl::DynamicFilterConfigProviderImpl( + FilterConfigSubscriptionSharedPtr&& subscription, + const std::set& require_type_urls, + Server::Configuration::FactoryContext& factory_context) + : subscription_(std::move(subscription)), require_type_urls_(require_type_urls), + tls_(factory_context.threadLocal().allocateSlot()), + init_target_("DynamicFilterConfigProviderImpl", [this]() { + subscription_->start(); + // This init target is used to activate the subscription but not wait + // for a response. It is used whenever a default config is provided to be + // used while waiting for a response. + init_target_.ready(); + }) { + subscription_->filter_config_providers_.insert(this); + tls_->set([](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr { + return std::make_shared(); + }); +} + +DynamicFilterConfigProviderImpl::~DynamicFilterConfigProviderImpl() { + subscription_->filter_config_providers_.erase(this); +} + +const std::string& DynamicFilterConfigProviderImpl::name() { return subscription_->name(); } + +absl::optional DynamicFilterConfigProviderImpl::config() { + return tls_->getTyped().config_; +} + +void DynamicFilterConfigProviderImpl::validateConfig( + const ProtobufWkt::Any& proto_config, Server::Configuration::NamedHttpFilterConfigFactory&) { + auto type_url = Config::Utility::getFactoryType(proto_config); + if (require_type_urls_.count(type_url) == 0) { + throw EnvoyException(fmt::format("Error: filter config has type URL {} but expect {}.", + type_url, absl::StrJoin(require_type_urls_, ", "))); + } +} + +void DynamicFilterConfigProviderImpl::onConfigUpdate(Envoy::Http::FilterFactoryCb config, + const std::string&) { + tls_->runOnAllThreads([config](ThreadLocal::ThreadLocalObjectSharedPtr previous) + -> ThreadLocal::ThreadLocalObjectSharedPtr { + auto prev_config = std::dynamic_pointer_cast(previous); + prev_config->config_ = config; + return previous; + }); +} + +FilterConfigSubscription::FilterConfigSubscription( + const envoy::config::core::v3::ConfigSource& config_source, + const std::string& filter_config_name, Server::Configuration::FactoryContext& factory_context, + const std::string& stat_prefix, FilterConfigProviderManagerImpl& filter_config_provider_manager, + const std::string& subscription_id) + : Config::SubscriptionBase( + envoy::config::core::v3::ApiVersion::V3, + factory_context.messageValidationContext().dynamicValidationVisitor(), "name"), + filter_config_name_(filter_config_name), factory_context_(factory_context), + validator_(factory_context.messageValidationContext().dynamicValidationVisitor()), + init_target_(fmt::format("FilterConfigSubscription init {}", filter_config_name_), + [this]() { start(); }), + scope_(factory_context.scope().createScope(stat_prefix + "extension_config_discovery." + + filter_config_name_ + ".")), + stat_prefix_(stat_prefix), + stats_({ALL_EXTENSION_CONFIG_DISCOVERY_STATS(POOL_COUNTER(*scope_))}), + filter_config_provider_manager_(filter_config_provider_manager), + subscription_id_(subscription_id) { + const auto resource_name = getResourceName(); + subscription_ = + factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( + config_source, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_); +} + +void FilterConfigSubscription::start() { + if (!started_) { + started_ = true; + subscription_->start({filter_config_name_}); + } +} + +void FilterConfigSubscription::onConfigUpdate( + const std::vector& resources, const std::string& version_info) { + // Make sure to make progress in case the control plane is temporarily inconsistent. + init_target_.ready(); + + if (resources.size() != 1) { + throw EnvoyException(fmt::format( + "Unexpected number of resources in ExtensionConfigDS response: {}", resources.size())); + } + const auto& filter_config = dynamic_cast( + resources[0].get().resource()); + if (filter_config.name() != filter_config_name_) { + throw EnvoyException(fmt::format("Unexpected resource name in ExtensionConfigDS response: {}", + filter_config.name())); + } + // Skip update if hash matches + const uint64_t new_hash = MessageUtil::hash(filter_config.typed_config()); + if (new_hash == last_config_hash_) { + return; + } + auto& factory = + Config::Utility::getAndCheckFactory( + filter_config); + // Ensure that the filter config is valid in the filter chain context once the proto is processed. + // Validation happens before updating to prevent a partial update application. It might be + // possible that the providers have distinct type URL constraints. + for (auto* provider : filter_config_providers_) { + provider->validateConfig(filter_config.typed_config(), factory); + } + ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig( + filter_config.typed_config(), validator_, factory); + Envoy::Http::FilterFactoryCb factory_callback = + factory.createFilterFactoryFromProto(*message, stat_prefix_, factory_context_); + ENVOY_LOG(debug, "Updating filter config {}", filter_config_name_); + for (auto* provider : filter_config_providers_) { + provider->onConfigUpdate(factory_callback, version_info); + } + stats_.config_reload_.inc(); + last_config_hash_ = new_hash; +} + +void FilterConfigSubscription::onConfigUpdate( + const std::vector& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, const std::string&) { + if (!removed_resources.empty()) { + ENVOY_LOG(error, + "Server sent a delta ExtensionConfigDS update attempting to remove a resource (name: " + "{}). Ignoring.", + removed_resources[0]); + } + if (!added_resources.empty()) { + onConfigUpdate(added_resources, added_resources[0].get().version()); + } +} + +void FilterConfigSubscription::onConfigUpdateFailed(Config::ConfigUpdateFailureReason reason, + const EnvoyException*) { + ENVOY_LOG(debug, "Updating filter config {} failed due to {}", filter_config_name_, reason); + stats_.config_fail_.inc(); + // Make sure to make progress in case the control plane is temporarily failing. + init_target_.ready(); +} + +FilterConfigSubscription::~FilterConfigSubscription() { + // If we get destroyed during initialization, make sure we signal that we "initialized". + init_target_.ready(); + // Remove the subscription from the provider manager. + filter_config_provider_manager_.subscriptions_.erase(subscription_id_); +} + +std::shared_ptr FilterConfigProviderManagerImpl::getSubscription( + const envoy::config::core::v3::ConfigSource& config_source, const std::string& name, + Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix) { + // 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 + // aliasing. + const std::string subscription_id = absl::StrCat(MessageUtil::hash(config_source), ".", name); + auto it = subscriptions_.find(subscription_id); + if (it == subscriptions_.end()) { + auto subscription = std::make_shared( + config_source, name, factory_context, stat_prefix, *this, subscription_id); + subscriptions_.insert({subscription_id, std::weak_ptr(subscription)}); + return subscription; + } else { + auto existing = it->second.lock(); + ASSERT(existing != nullptr, + absl::StrCat("Cannot find subscribed filter config resource ", name)); + return existing; + } +} + +FilterConfigProviderPtr FilterConfigProviderManagerImpl::createDynamicFilterConfigProvider( + const envoy::config::core::v3::ConfigSource& config_source, + const std::string& filter_config_name, const std::set& require_type_urls, + Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix, + bool apply_without_warming) { + auto subscription = + getSubscription(config_source, filter_config_name, factory_context, stat_prefix); + // For warming, wait until the subscription receives the first response to indicate readiness. + // Otherwise, mark ready immediately and start the subscription on initialization. A default + // config is expected in the latter case. + if (!apply_without_warming) { + factory_context.initManager().add(subscription->initTarget()); + } + auto provider = std::make_unique( + std::move(subscription), require_type_urls, factory_context); + // Ensure the subscription starts if it has not already. + if (apply_without_warming) { + factory_context.initManager().add(provider->init_target_); + } + return provider; +} + +} // namespace Http +} // namespace Filter +} // namespace Envoy diff --git a/source/common/filter/http/filter_config_discovery_impl.h b/source/common/filter/http/filter_config_discovery_impl.h new file mode 100644 index 000000000000..1c2c838c5aae --- /dev/null +++ b/source/common/filter/http/filter_config_discovery_impl.h @@ -0,0 +1,184 @@ +#pragma once + +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/config/core/v3/extension.pb.validate.h" +#include "envoy/config/subscription.h" +#include "envoy/filter/http/filter_config_provider.h" +#include "envoy/protobuf/message_validator.h" +#include "envoy/server/factory_context.h" +#include "envoy/singleton/instance.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +#include "common/config/subscription_base.h" +#include "common/init/manager_impl.h" +#include "common/init/target_impl.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" + +namespace Envoy { +namespace Filter { +namespace Http { + +class FilterConfigProviderManagerImpl; +class FilterConfigSubscription; + +using FilterConfigSubscriptionSharedPtr = std::shared_ptr; + +/** + * Implementation of a filter config provider using discovery subscriptions. + **/ +class DynamicFilterConfigProviderImpl : public FilterConfigProvider { +public: + DynamicFilterConfigProviderImpl(FilterConfigSubscriptionSharedPtr&& subscription, + const std::set& require_type_urls, + Server::Configuration::FactoryContext& factory_context); + ~DynamicFilterConfigProviderImpl() override; + + // Config::ExtensionConfigProvider + const std::string& name() override; + absl::optional config() override; + void validateConfig(const ProtobufWkt::Any& proto_config, + Server::Configuration::NamedHttpFilterConfigFactory&) override; + void onConfigUpdate(Envoy::Http::FilterFactoryCb config, const std::string&) override; + +private: + struct ThreadLocalConfig : public ThreadLocal::ThreadLocalObject { + ThreadLocalConfig() : config_{absl::nullopt} {} + absl::optional config_{}; + }; + + FilterConfigSubscriptionSharedPtr subscription_; + const std::set require_type_urls_; + ThreadLocal::SlotPtr tls_; + + // Local initialization target to ensure that the subscription starts in + // case no warming is requested by any other filter config provider. + Init::TargetImpl init_target_; + + friend class FilterConfigProviderManagerImpl; +}; + +/** + * All extension config discovery stats. @see stats_macros.h + */ +#define ALL_EXTENSION_CONFIG_DISCOVERY_STATS(COUNTER) \ + COUNTER(config_reload) \ + COUNTER(config_fail) + +/** + * Struct definition for all extension config discovery stats. @see stats_macros.h + */ +struct ExtensionConfigDiscoveryStats { + ALL_EXTENSION_CONFIG_DISCOVERY_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * A class that fetches the filter configuration dynamically using the filter config discovery API. + * Subscriptions are shared between the filter config providers. The filter config providers are + * notified when a new config is accepted. + */ +class FilterConfigSubscription + : Config::SubscriptionBase, + Logger::Loggable { +public: + FilterConfigSubscription(const envoy::config::core::v3::ConfigSource& config_source, + const std::string& filter_config_name, + Server::Configuration::FactoryContext& factory_context, + const std::string& stat_prefix, + FilterConfigProviderManagerImpl& filter_config_provider_manager, + const std::string& subscription_id); + + ~FilterConfigSubscription() override; + + const Init::SharedTargetImpl& initTarget() { return init_target_; } + const std::string& name() { return filter_config_name_; } + +private: + void start(); + + // Config::SubscriptionCallbacks + void onConfigUpdate(const std::vector& resources, + const std::string& version_info) override; + void onConfigUpdate(const std::vector& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string&) override; + void onConfigUpdateFailed(Config::ConfigUpdateFailureReason reason, + const EnvoyException*) override; + + std::unique_ptr subscription_; + const std::string filter_config_name_; + uint64_t last_config_hash_{0ul}; + Server::Configuration::FactoryContext& factory_context_; + ProtobufMessage::ValidationVisitor& validator_; + + Init::SharedTargetImpl init_target_; + bool started_{false}; + + Stats::ScopePtr scope_; + const std::string stat_prefix_; + ExtensionConfigDiscoveryStats stats_; + + // FilterConfigProviderManagerImpl maintains active subscriptions in a map. + FilterConfigProviderManagerImpl& filter_config_provider_manager_; + const std::string subscription_id_; + absl::flat_hash_set filter_config_providers_; + friend class DynamicFilterConfigProviderImpl; +}; + +/** + * Provider implementation of a static filter config. + **/ +class StaticFilterConfigProviderImpl : public FilterConfigProvider { +public: + StaticFilterConfigProviderImpl(const Envoy::Http::FilterFactoryCb& config, + const std::string filter_config_name) + : config_(config), filter_config_name_(filter_config_name) {} + + // Config::ExtensionConfigProvider + const std::string& name() override { return filter_config_name_; } + absl::optional config() override { return config_; } + void validateConfig(const ProtobufWkt::Any&, + Server::Configuration::NamedHttpFilterConfigFactory&) override { + NOT_REACHED_GCOVR_EXCL_LINE; + } + void onConfigUpdate(Envoy::Http::FilterFactoryCb, const std::string&) override { + NOT_REACHED_GCOVR_EXCL_LINE; + } + +private: + Envoy::Http::FilterFactoryCb config_; + const std::string filter_config_name_; +}; + +/** + * An implementation of FilterConfigProviderManager. + */ +class FilterConfigProviderManagerImpl : public FilterConfigProviderManager, + public Singleton::Instance { +public: + FilterConfigProviderPtr createDynamicFilterConfigProvider( + const envoy::config::core::v3::ConfigSource& config_source, + const std::string& filter_config_name, const std::set& require_type_urls, + Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix, + bool apply_without_warming) override; + + FilterConfigProviderPtr + createStaticFilterConfigProvider(const Envoy::Http::FilterFactoryCb& config, + const std::string& filter_config_name) override { + return std::make_unique(config, filter_config_name); + } + +private: + std::shared_ptr + getSubscription(const envoy::config::core::v3::ConfigSource& config_source, + const std::string& name, Server::Configuration::FactoryContext& factory_context, + const std::string& stat_prefix); + absl::flat_hash_map> subscriptions_; + friend class FilterConfigSubscription; +}; + +} // namespace Http +} // namespace Filter +} // namespace Envoy diff --git a/source/common/stream_info/utility.cc b/source/common/stream_info/utility.cc index 9a5a690b682f..2f7049545bd3 100644 --- a/source/common/stream_info/utility.cc +++ b/source/common/stream_info/utility.cc @@ -27,6 +27,7 @@ const std::string ResponseFlagUtils::INVALID_ENVOY_REQUEST_HEADERS = "IH"; const std::string ResponseFlagUtils::DOWNSTREAM_PROTOCOL_ERROR = "DPE"; const std::string ResponseFlagUtils::UPSTREAM_MAX_STREAM_DURATION_REACHED = "UMSDR"; const std::string ResponseFlagUtils::RESPONSE_FROM_CACHE_FILTER = "RFCF"; +const std::string ResponseFlagUtils::NO_FILTER_CONFIG_FOUND = "NFCF"; void ResponseFlagUtils::appendString(std::string& result, const std::string& append) { if (result.empty()) { @@ -39,7 +40,7 @@ void ResponseFlagUtils::appendString(std::string& result, const std::string& app const std::string ResponseFlagUtils::toShortString(const StreamInfo& stream_info) { std::string result; - static_assert(ResponseFlag::LastFlag == 0x100000, "A flag has been added. Fix this code."); + static_assert(ResponseFlag::LastFlag == 0x200000, "A flag has been added. Fix this code."); if (stream_info.hasResponseFlag(ResponseFlag::FailedLocalHealthCheck)) { appendString(result, FAILED_LOCAL_HEALTH_CHECK); @@ -124,6 +125,10 @@ const std::string ResponseFlagUtils::toShortString(const StreamInfo& stream_info appendString(result, RESPONSE_FROM_CACHE_FILTER); } + if (stream_info.hasResponseFlag(ResponseFlag::NoFilterConfigFound)) { + appendString(result, NO_FILTER_CONFIG_FOUND); + } + return result.empty() ? NONE : result; } @@ -153,6 +158,7 @@ absl::optional ResponseFlagUtils::toResponseFlag(const std::string {ResponseFlagUtils::UPSTREAM_MAX_STREAM_DURATION_REACHED, ResponseFlag::UpstreamMaxStreamDurationReached}, {ResponseFlagUtils::RESPONSE_FROM_CACHE_FILTER, ResponseFlag::ResponseFromCacheFilter}, + {ResponseFlagUtils::NO_FILTER_CONFIG_FOUND, ResponseFlag::NoFilterConfigFound}, }; const auto& it = map.find(flag); if (it != map.end()) { diff --git a/source/common/stream_info/utility.h b/source/common/stream_info/utility.h index 2c7b73d751fb..9b4ac08e413c 100644 --- a/source/common/stream_info/utility.h +++ b/source/common/stream_info/utility.h @@ -42,6 +42,7 @@ class ResponseFlagUtils { const static std::string DOWNSTREAM_PROTOCOL_ERROR; const static std::string UPSTREAM_MAX_STREAM_DURATION_REACHED; const static std::string RESPONSE_FROM_CACHE_FILTER; + const static std::string NO_FILTER_CONFIG_FOUND; }; /** diff --git a/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc b/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc index 0977540b4102..74b061cbad7c 100644 --- a/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc +++ b/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc @@ -37,7 +37,7 @@ void Utility::responseFlagsToAccessLogResponseFlags( envoy::data::accesslog::v3::AccessLogCommon& common_access_log, const StreamInfo::StreamInfo& stream_info) { - static_assert(StreamInfo::ResponseFlag::LastFlag == 0x100000, + static_assert(StreamInfo::ResponseFlag::LastFlag == 0x200000, "A flag has been added. Fix this code."); if (stream_info.hasResponseFlag(StreamInfo::ResponseFlag::FailedLocalHealthCheck)) { @@ -122,6 +122,9 @@ void Utility::responseFlagsToAccessLogResponseFlags( if (stream_info.hasResponseFlag(StreamInfo::ResponseFlag::ResponseFromCacheFilter)) { common_access_log.mutable_response_flags()->set_response_from_cache_filter(true); } + if (stream_info.hasResponseFlag(StreamInfo::ResponseFlag::NoFilterConfigFound)) { + common_access_log.mutable_response_flags()->set_no_filter_config_found(true); + } } void Utility::extractCommonAccessLogProperties( diff --git a/source/extensions/filters/network/http_connection_manager/BUILD b/source/extensions/filters/network/http_connection_manager/BUILD index 7ab7817d80fd..5d03f03ecc4a 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -31,6 +31,7 @@ envoy_cc_extension( "//source/common/access_log:access_log_lib", "//source/common/common:minimal_logger_lib", "//source/common/config:utility_lib", + "//source/common/filter/http:filter_config_discovery_lib", "//source/common/http:conn_manager_lib", "//source/common/http:default_server_string_lib", "//source/common/http:request_id_extension_lib", @@ -48,6 +49,7 @@ envoy_cc_extension( "//source/common/tracing:http_tracer_config_lib", "//source/common/tracing:http_tracer_lib", "//source/common/tracing:http_tracer_manager_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", "//source/extensions/filters/network:well_known_names", "//source/extensions/filters/network/common:factory_base_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 888709fe4251..ce274734447a 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -9,6 +9,7 @@ #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h" #include "envoy/filesystem/filesystem.h" +#include "envoy/registry/registry.h" #include "envoy/server/admin.h" #include "envoy/tracing/http_tracer.h" #include "envoy/type/tracing/v3/custom_tag.pb.h" @@ -17,6 +18,7 @@ #include "common/access_log/access_log_impl.h" #include "common/common/fmt.h" #include "common/config/utility.h" +#include "common/filter/http/filter_config_discovery_impl.h" #include "common/http/conn_manager_utility.h" #include "common/http/default_server_string.h" #include "common/http/http1/codec_impl.h" @@ -35,6 +37,8 @@ #include "common/tracing/http_tracer_config_impl.h" #include "common/tracing/http_tracer_manager_impl.h" +#include "extensions/filters/http/common/pass_through_filter.h" + namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -75,6 +79,16 @@ std::unique_ptr createInternalAddressConfig( return std::make_unique(); } +class MissingConfigFilter : public Http::PassThroughDecoderFilter { +public: + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap&, bool) override { + decoder_callbacks_->streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoFilterConfigFound); + decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, EMPTY_STRING, nullptr, + absl::nullopt, EMPTY_STRING); + return Http::FilterHeadersStatus::StopIteration; + } +}; + } // namespace // Singleton registration via macro defined in envoy/singleton/manager.h @@ -82,6 +96,7 @@ SINGLETON_MANAGER_REGISTRATION(date_provider); SINGLETON_MANAGER_REGISTRATION(route_config_provider_manager); SINGLETON_MANAGER_REGISTRATION(scoped_routes_config_provider_manager); SINGLETON_MANAGER_REGISTRATION(http_tracer_manager); +SINGLETON_MANAGER_REGISTRATION(filter_config_provider_manager); Utility::Singletons Utility::createSingletons(Server::Configuration::FactoryContext& context) { std::shared_ptr date_provider = @@ -112,8 +127,13 @@ Utility::Singletons Utility::createSingletons(Server::Configuration::FactoryCont context.getServerFactoryContext(), context.messageValidationVisitor())); }); + std::shared_ptr filter_config_provider_manager = + context.singletonManager().getTyped( + SINGLETON_MANAGER_REGISTERED_NAME(filter_config_provider_manager), + [] { return std::make_shared(); }); + return {date_provider, route_config_provider_manager, scoped_routes_config_provider_manager, - http_tracer_manager}; + http_tracer_manager, filter_config_provider_manager}; } std::shared_ptr Utility::createConfig( @@ -122,10 +142,11 @@ std::shared_ptr Utility::createConfig( Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider, Router::RouteConfigProviderManager& route_config_provider_manager, Config::ConfigProviderManager& scoped_routes_config_provider_manager, - Tracing::HttpTracerManager& http_tracer_manager) { + Tracing::HttpTracerManager& http_tracer_manager, + Filter::Http::FilterConfigProviderManager& filter_config_provider_manager) { return std::make_shared( proto_config, context, date_provider, route_config_provider_manager, - scoped_routes_config_provider_manager, http_tracer_manager); + scoped_routes_config_provider_manager, http_tracer_manager, filter_config_provider_manager); } Network::FilterFactoryCb @@ -137,7 +158,8 @@ HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped( auto filter_config = Utility::createConfig( proto_config, context, *singletons.date_provider_, *singletons.route_config_provider_manager_, - *singletons.scoped_routes_config_provider_manager_, *singletons.http_tracer_manager_); + *singletons.scoped_routes_config_provider_manager_, *singletons.http_tracer_manager_, + *singletons.filter_config_provider_manager_); // This lambda captures the shared_ptrs created above, thus preserving the // reference count. @@ -169,7 +191,8 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider, Router::RouteConfigProviderManager& route_config_provider_manager, Config::ConfigProviderManager& scoped_routes_config_provider_manager, - Tracing::HttpTracerManager& http_tracer_manager) + Tracing::HttpTracerManager& http_tracer_manager, + Filter::Http::FilterConfigProviderManager& filter_config_provider_manager) : context_(context), stats_prefix_(fmt::format("http.{}.", config.stat_prefix())), stats_(Http::ConnectionManagerImpl::generateStats(stats_prefix_, context_.scope())), tracing_stats_( @@ -180,6 +203,7 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( skip_xff_append_(config.skip_xff_append()), via_(config.via()), route_config_provider_manager_(route_config_provider_manager), scoped_routes_config_provider_manager_(scoped_routes_config_provider_manager), + filter_config_provider_manager_(filter_config_provider_manager), http2_options_(Http2::Utility::initializeAndValidateOptions( config.http2_protocol_options(), config.has_stream_error_on_invalid_http_message(), config.stream_error_on_invalid_http_message())), @@ -451,17 +475,16 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( void HttpConnectionManagerConfig::processFilter( const envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter& proto_config, - int i, absl::string_view prefix, std::list& filter_factories, + int i, absl::string_view prefix, FilterFactoriesList& filter_factories, const char* filter_chain_type, bool last_filter_in_current_config) { ENVOY_LOG(debug, " {} filter #{}", prefix, i); - ENVOY_LOG(debug, " name: {}", proto_config.name()); - ENVOY_LOG(debug, " config: {}", - MessageUtil::getJsonStringFromMessage( - proto_config.has_typed_config() - ? static_cast(proto_config.typed_config()) - : static_cast( - proto_config.hidden_envoy_deprecated_config()), - true)); + if (proto_config.config_type_case() == + envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter::ConfigTypeCase:: + kConfigDiscovery) { + processDynamicFilterConfig(proto_config.name(), proto_config.config_discovery(), + filter_factories, filter_chain_type, last_filter_in_current_config); + return; + } // Now see if there is a factory that will accept the config. auto& factory = @@ -474,7 +497,63 @@ void HttpConnectionManagerConfig::processFilter( bool is_terminal = factory.isTerminalFilter(); Config::Utility::validateTerminalFilters(proto_config.name(), factory.name(), filter_chain_type, is_terminal, last_filter_in_current_config); - filter_factories.push_back(callback); + auto filter_config_provider = filter_config_provider_manager_.createStaticFilterConfigProvider( + callback, proto_config.name()); + ENVOY_LOG(debug, " name: {}", filter_config_provider->name()); + ENVOY_LOG(debug, " config: {}", + MessageUtil::getJsonStringFromMessage( + proto_config.has_typed_config() + ? static_cast(proto_config.typed_config()) + : static_cast( + proto_config.hidden_envoy_deprecated_config()), + true)); + filter_factories.push_back(std::move(filter_config_provider)); +} + +void HttpConnectionManagerConfig::processDynamicFilterConfig( + const std::string& name, const envoy::config::core::v3::ExtensionConfigSource& config_discovery, + FilterFactoriesList& filter_factories, const char* filter_chain_type, + bool last_filter_in_current_config) { + ENVOY_LOG(debug, " dynamic filter name: {}", name); + if (config_discovery.apply_default_config_without_warming() && + !config_discovery.has_default_config()) { + throw EnvoyException(fmt::format( + "Error: filter config {} applied without warming but has no default config.", name)); + } + std::set require_type_urls; + for (const auto& type_url : config_discovery.type_urls()) { + auto factory_type_url = TypeUtil::typeUrlToDescriptorFullName(type_url); + require_type_urls.emplace(factory_type_url); + auto* factory = Registry::FactoryRegistry< + Server::Configuration::NamedHttpFilterConfigFactory>::getFactoryByType(factory_type_url); + if (factory == nullptr) { + throw EnvoyException( + fmt::format("Error: no factory found for a required type URL {}.", factory_type_url)); + } + Config::Utility::validateTerminalFilters(name, factory->name(), filter_chain_type, + factory->isTerminalFilter(), + last_filter_in_current_config); + } + auto filter_config_provider = filter_config_provider_manager_.createDynamicFilterConfigProvider( + config_discovery.config_source(), name, require_type_urls, context_, stats_prefix_, + config_discovery.apply_default_config_without_warming()); + if (config_discovery.has_default_config()) { + auto* default_factory = + Config::Utility::getFactoryByType( + config_discovery.default_config()); + if (default_factory == nullptr) { + throw EnvoyException(fmt::format("Error: cannot find filter factory {} for default filter " + "configuration with type URL {}.", + name, config_discovery.default_config().type_url())); + } + filter_config_provider->validateConfig(config_discovery.default_config(), *default_factory); + ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig( + config_discovery.default_config(), context_.messageValidationVisitor(), *default_factory); + Http::FilterFactoryCb default_config = + default_factory->createFilterFactoryFromProto(*message, stats_prefix_, context_); + filter_config_provider->onConfigUpdate(default_config, ""); + } + filter_factories.push_back(std::move(filter_config_provider)); } Http::ServerConnectionPtr @@ -528,12 +607,32 @@ HttpConnectionManagerConfig::createCodec(Network::Connection& connection, NOT_REACHED_GCOVR_EXCL_LINE; } -void HttpConnectionManagerConfig::createFilterChain(Http::FilterChainFactoryCallbacks& callbacks) { - for (const Http::FilterFactoryCb& factory : filter_factories_) { - factory(callbacks); +void HttpConnectionManagerConfig::createFilterChainForFactories( + Http::FilterChainFactoryCallbacks& callbacks, const FilterFactoriesList& filter_factories) { + bool added_missing_config_filter = false; + for (const auto& filter_config_provider : filter_factories) { + auto config = filter_config_provider->config(); + if (config.has_value()) { + config.value()(callbacks); + continue; + } + + // If a filter config is missing after warming, inject a local reply with status 500. + if (!added_missing_config_filter) { + ENVOY_LOG(trace, "Missing filter config for a provider {}", filter_config_provider->name()); + callbacks.addStreamDecoderFilter( + Http::StreamDecoderFilterSharedPtr{std::make_shared()}); + added_missing_config_filter = true; + } else { + ENVOY_LOG(trace, "Provider {} missing a filter config", filter_config_provider->name()); + } } } +void HttpConnectionManagerConfig::createFilterChain(Http::FilterChainFactoryCallbacks& callbacks) { + createFilterChainForFactories(callbacks, filter_factories_); +} + bool HttpConnectionManagerConfig::createUpgradeFilterChain( absl::string_view upgrade_type, const Http::FilterChainFactory::UpgradeMap* per_route_upgrade_map, @@ -562,9 +661,7 @@ bool HttpConnectionManagerConfig::createUpgradeFilterChain( filters_to_use = it->second.filter_factories.get(); } - for (const Http::FilterFactoryCb& factory : *filters_to_use) { - factory(callbacks); - } + createFilterChainForFactories(callbacks, *filters_to_use); return true; } @@ -602,7 +699,8 @@ HttpConnectionManagerFactory::createHttpConnectionManagerFactoryFromProto( auto filter_config = Utility::createConfig( proto_config, context, *singletons.date_provider_, *singletons.route_config_provider_manager_, - *singletons.scoped_routes_config_provider_manager_, *singletons.http_tracer_manager_); + *singletons.scoped_routes_config_provider_manager_, *singletons.http_tracer_manager_, + *singletons.filter_config_provider_manager_); // This lambda captures the shared_ptrs created above, thus preserving the // reference count. diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index d2fef63dedb1..47cc707bdb89 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -8,8 +8,10 @@ #include #include "envoy/config/config_provider_manager.h" +#include "envoy/config/core/v3/extension.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h" +#include "envoy/filter/http/filter_config_provider.h" #include "envoy/http/filter.h" #include "envoy/http/request_id_extension.h" #include "envoy/router/route_config_provider_manager.h" @@ -88,11 +90,12 @@ class HttpConnectionManagerConfig : Logger::Loggable, Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider, Router::RouteConfigProviderManager& route_config_provider_manager, Config::ConfigProviderManager& scoped_routes_config_provider_manager, - Tracing::HttpTracerManager& http_tracer_manager); + Tracing::HttpTracerManager& http_tracer_manager, + Filter::Http::FilterConfigProviderManager& filter_config_provider_manager); // Http::FilterChainFactory void createFilterChain(Http::FilterChainFactoryCallbacks& callbacks) override; - using FilterFactoriesList = std::list; + using FilterFactoriesList = std::list; struct FilterConfig { std::unique_ptr filter_factories; bool allow_upgrade; @@ -178,6 +181,13 @@ class HttpConnectionManagerConfig : Logger::Loggable, proto_config, int i, absl::string_view prefix, FilterFactoriesList& filter_factories, const char* filter_chain_type, bool last_filter_in_current_config); + void + processDynamicFilterConfig(const std::string& name, + const envoy::config::core::v3::ExtensionConfigSource& config_discovery, + FilterFactoriesList& filter_factories, const char* filter_chain_type, + bool last_filter_in_current_config); + void createFilterChainForFactories(Http::FilterChainFactoryCallbacks& callbacks, + const FilterFactoriesList& filter_factories); /** * Determines what tracing provider to use for a given @@ -206,6 +216,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, std::vector set_current_client_cert_details_; Router::RouteConfigProviderManager& route_config_provider_manager_; Config::ConfigProviderManager& scoped_routes_config_provider_manager_; + Filter::Http::FilterConfigProviderManager& filter_config_provider_manager_; CodecType codec_type_; envoy::config::core::v3::Http2ProtocolOptions http2_options_; const Http::Http1Settings http1_settings_; @@ -267,6 +278,7 @@ class Utility { Router::RouteConfigProviderManagerSharedPtr route_config_provider_manager_; Router::ScopedRoutesConfigProviderManagerSharedPtr scoped_routes_config_provider_manager_; Tracing::HttpTracerManagerSharedPtr http_tracer_manager_; + std::shared_ptr filter_config_provider_manager_; }; /** @@ -293,7 +305,8 @@ class Utility { Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider, Router::RouteConfigProviderManager& route_config_provider_manager, Config::ConfigProviderManager& scoped_routes_config_provider_manager, - Tracing::HttpTracerManager& http_tracer_manager); + Tracing::HttpTracerManager& http_tracer_manager, + Filter::Http::FilterConfigProviderManager& filter_config_provider_manager); }; } // namespace HttpConnectionManager diff --git a/test/common/access_log/access_log_impl_test.cc b/test/common/access_log/access_log_impl_test.cc index 0bcac1bd72e5..09abacf4dc69 100644 --- a/test/common/access_log/access_log_impl_test.cc +++ b/test/common/access_log/access_log_impl_test.cc @@ -948,12 +948,13 @@ name: accesslog - DPE - UMSDR - RFCF + - NFCF typed_config: "@type": type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/null )EOF"; - static_assert(StreamInfo::ResponseFlag::LastFlag == 0x100000, + static_assert(StreamInfo::ResponseFlag::LastFlag == 0x200000, "A flag has been added. Fix this code."); const std::vector all_response_flags = { @@ -977,7 +978,8 @@ name: accesslog StreamInfo::ResponseFlag::InvalidEnvoyRequestHeaders, StreamInfo::ResponseFlag::DownstreamProtocolError, StreamInfo::ResponseFlag::UpstreamMaxStreamDurationReached, - StreamInfo::ResponseFlag::ResponseFromCacheFilter}; + StreamInfo::ResponseFlag::ResponseFromCacheFilter, + StreamInfo::ResponseFlag::NoFilterConfigFound}; InstanceSharedPtr log = AccessLogFactory::fromProto(parseAccessLogFromV3Yaml(yaml), context_); @@ -1009,7 +1011,8 @@ name: accesslog "[\"embedded message failed validation\"] | caused by " "ResponseFlagFilterValidationError.Flags[i]: [\"value must be in list \" [\"LH\" \"UH\" " "\"UT\" \"LR\" \"UR\" \"UF\" \"UC\" \"UO\" \"NR\" \"DI\" \"FI\" \"RL\" \"UAEX\" \"RLSE\" " - "\"DC\" \"URX\" \"SI\" \"IH\" \"DPE\" \"UMSDR\" \"RFCF\"]]): name: \"accesslog\"\nfilter {\n " + "\"DC\" \"URX\" \"SI\" \"IH\" \"DPE\" \"UMSDR\" \"RFCF\" \"NFCF\"]]): name: " + "\"accesslog\"\nfilter {\n " " " "response_flag_filter {\n flags: \"UnsupportedFlag\"\n }\n}\ntyped_config {\n " "[type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog] {\n path: \"/dev/null\"\n " @@ -1036,7 +1039,8 @@ name: accesslog "[\"embedded message failed validation\"] | caused by " "ResponseFlagFilterValidationError.Flags[i]: [\"value must be in list \" [\"LH\" \"UH\" " "\"UT\" \"LR\" \"UR\" \"UF\" \"UC\" \"UO\" \"NR\" \"DI\" \"FI\" \"RL\" \"UAEX\" \"RLSE\" " - "\"DC\" \"URX\" \"SI\" \"IH\" \"DPE\" \"UMSDR\" \"RFCF\"]]): name: \"accesslog\"\nfilter {\n " + "\"DC\" \"URX\" \"SI\" \"IH\" \"DPE\" \"UMSDR\" \"RFCF\" \"NFCF\"]]): name: " + "\"accesslog\"\nfilter {\n " " " "response_flag_filter {\n flags: \"UnsupportedFlag\"\n }\n}\ntyped_config {\n " "[type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog] {\n path: \"/dev/null\"\n " diff --git a/test/common/filter/http/BUILD b/test/common/filter/http/BUILD new file mode 100644 index 000000000000..c6ce0344543c --- /dev/null +++ b/test/common/filter/http/BUILD @@ -0,0 +1,30 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "filter_config_discovery_impl_test", + srcs = ["filter_config_discovery_impl_test.cc"], + deps = [ + "//source/common/config:utility_lib", + "//source/common/filter/http:filter_config_discovery_lib", + "//source/common/json:json_loader_lib", + "//source/extensions/filters/http/health_check:config", + "//source/extensions/filters/http/router:config", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/protobuf:protobuf_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/service/discovery/v3:pkg_cc_proto", + ], +) diff --git a/test/common/filter/http/filter_config_discovery_impl_test.cc b/test/common/filter/http/filter_config_discovery_impl_test.cc new file mode 100644 index 000000000000..bd25e662a593 --- /dev/null +++ b/test/common/filter/http/filter_config_discovery_impl_test.cc @@ -0,0 +1,297 @@ +#include +#include +#include + +#include "envoy/config/core/v3/config_source.pb.h" +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/config/core/v3/extension.pb.validate.h" +#include "envoy/service/discovery/v3/discovery.pb.h" +#include "envoy/stats/scope.h" + +#include "common/config/utility.h" +#include "common/filter/http/filter_config_discovery_impl.h" +#include "common/json/json_loader.h" + +#include "test/mocks/init/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/protobuf/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/printers.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::InSequence; +using testing::Invoke; +using testing::ReturnRef; + +namespace Envoy { +namespace Filter { +namespace Http { +namespace { + +class FilterConfigDiscoveryTestBase : public testing::Test { +public: + FilterConfigDiscoveryTestBase() { + // For server_factory_context + ON_CALL(factory_context_, scope()).WillByDefault(ReturnRef(scope_)); + ON_CALL(factory_context_, messageValidationContext()) + .WillByDefault(ReturnRef(validation_context_)); + EXPECT_CALL(validation_context_, dynamicValidationVisitor()) + .WillRepeatedly(ReturnRef(validation_visitor_)); + EXPECT_CALL(factory_context_, initManager()).WillRepeatedly(ReturnRef(init_manager_)); + ON_CALL(init_manager_, add(_)).WillByDefault(Invoke([this](const Init::Target& target) { + init_target_handle_ = target.createHandle("test"); + })); + ON_CALL(init_manager_, initialize(_)) + .WillByDefault(Invoke( + [this](const Init::Watcher& watcher) { init_target_handle_->initialize(watcher); })); + } + + Event::SimulatedTimeSystem& timeSystem() { return time_system_; } + + Event::SimulatedTimeSystem time_system_; + NiceMock validation_context_; + NiceMock validation_visitor_; + NiceMock init_manager_; + NiceMock factory_context_; + Init::ExpectableWatcherImpl init_watcher_; + Init::TargetHandlePtr init_target_handle_; + NiceMock scope_; +}; + +// Test base class with a single provider. +class FilterConfigDiscoveryImplTest : public FilterConfigDiscoveryTestBase { +public: + FilterConfigDiscoveryImplTest() { + filter_config_provider_manager_ = std::make_unique(); + } + ~FilterConfigDiscoveryImplTest() override { factory_context_.thread_local_.shutdownThread(); } + + FilterConfigProviderPtr createProvider(std::string name, bool warm) { + EXPECT_CALL(init_manager_, add(_)); + envoy::config::core::v3::ConfigSource config_source; + TestUtility::loadFromYaml("ads: {}", config_source); + return filter_config_provider_manager_->createDynamicFilterConfigProvider( + config_source, name, {"envoy.extensions.filters.http.router.v3.Router"}, factory_context_, + "xds.", !warm); + } + + void setup(bool warm = true) { + provider_ = createProvider("foo", warm); + callbacks_ = factory_context_.cluster_manager_.subscription_factory_.callbacks_; + EXPECT_CALL(*factory_context_.cluster_manager_.subscription_factory_.subscription_, start(_)); + if (!warm) { + EXPECT_CALL(init_watcher_, ready()); + } + init_manager_.initialize(init_watcher_); + } + + std::unique_ptr filter_config_provider_manager_; + FilterConfigProviderPtr provider_; + Config::SubscriptionCallbacks* callbacks_{}; +}; + +TEST_F(FilterConfigDiscoveryImplTest, DestroyReady) { + setup(); + EXPECT_CALL(init_watcher_, ready()); +} + +TEST_F(FilterConfigDiscoveryImplTest, Basic) { + InSequence s; + setup(); + EXPECT_EQ("foo", provider_->name()); + EXPECT_EQ(absl::nullopt, provider_->config()); + + // Initial request. + { + const std::string response_yaml = R"EOF( + version_info: "1" + resources: + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + + EXPECT_CALL(init_watcher_, ready()); + callbacks_->onConfigUpdate(decoded_resources.refvec_, response.version_info()); + EXPECT_NE(absl::nullopt, provider_->config()); + EXPECT_EQ(1UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_fail").value()); + } + + // 2nd request with same response. Based on hash should not reload config. + { + const std::string response_yaml = R"EOF( + version_info: "2" + resources: + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + callbacks_->onConfigUpdate(decoded_resources.refvec_, response.version_info()); + EXPECT_EQ(1UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_fail").value()); + } +} + +TEST_F(FilterConfigDiscoveryImplTest, ConfigFailed) { + InSequence s; + setup(); + EXPECT_CALL(init_watcher_, ready()); + callbacks_->onConfigUpdateFailed(Config::ConfigUpdateFailureReason::FetchTimedout, {}); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); + EXPECT_EQ(1UL, scope_.counter("xds.extension_config_discovery.foo.config_fail").value()); +} + +TEST_F(FilterConfigDiscoveryImplTest, TooManyResources) { + InSequence s; + setup(); + const std::string response_yaml = R"EOF( + version_info: "1" + resources: + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + EXPECT_CALL(init_watcher_, ready()); + EXPECT_THROW_WITH_MESSAGE( + callbacks_->onConfigUpdate(decoded_resources.refvec_, response.version_info()), + EnvoyException, "Unexpected number of resources in ExtensionConfigDS response: 2"); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); +} + +TEST_F(FilterConfigDiscoveryImplTest, WrongName) { + InSequence s; + setup(); + const std::string response_yaml = R"EOF( + version_info: "1" + resources: + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: bar + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + EXPECT_CALL(init_watcher_, ready()); + EXPECT_THROW_WITH_MESSAGE( + callbacks_->onConfigUpdate(decoded_resources.refvec_, response.version_info()), + EnvoyException, "Unexpected resource name in ExtensionConfigDS response: bar"); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); +} + +TEST_F(FilterConfigDiscoveryImplTest, Incremental) { + InSequence s; + setup(); + const std::string response_yaml = R"EOF( +version_info: "1" +resources: +- "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +)EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + Protobuf::RepeatedPtrField remove; + *remove.Add() = "bar"; + EXPECT_CALL(init_watcher_, ready()); + callbacks_->onConfigUpdate(decoded_resources.refvec_, remove, response.version_info()); + EXPECT_NE(absl::nullopt, provider_->config()); + EXPECT_EQ(1UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_fail").value()); +} + +TEST_F(FilterConfigDiscoveryImplTest, ApplyWithoutWarming) { + InSequence s; + setup(false); + EXPECT_EQ("foo", provider_->name()); + EXPECT_EQ(absl::nullopt, provider_->config()); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_fail").value()); +} + +TEST_F(FilterConfigDiscoveryImplTest, DualProviders) { + InSequence s; + setup(); + auto provider2 = createProvider("foo", true); + EXPECT_EQ("foo", provider2->name()); + EXPECT_EQ(absl::nullopt, provider2->config()); + const std::string response_yaml = R"EOF( + version_info: "1" + resources: + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + EXPECT_CALL(init_watcher_, ready()); + callbacks_->onConfigUpdate(decoded_resources.refvec_, response.version_info()); + EXPECT_NE(absl::nullopt, provider_->config()); + EXPECT_NE(absl::nullopt, provider2->config()); + EXPECT_EQ(1UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); +} + +TEST_F(FilterConfigDiscoveryImplTest, DualProvidersInvalid) { + InSequence s; + setup(); + auto provider2 = createProvider("foo", true); + const std::string response_yaml = R"EOF( + version_info: "1" + resources: + - "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: foo + typed_config: + "@type": type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck + pass_through_mode: false + )EOF"; + const auto response = + TestUtility::parseYaml(response_yaml); + const auto decoded_resources = + TestUtility::decodeResources(response); + EXPECT_CALL(init_watcher_, ready()); + EXPECT_THROW_WITH_MESSAGE( + callbacks_->onConfigUpdate(decoded_resources.refvec_, response.version_info()), + EnvoyException, + "Error: filter config has type URL envoy.config.filter.http.health_check.v2.HealthCheck but " + "expect envoy.extensions.filters.http.router.v3.Router."); + EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value()); +} + +} // namespace +} // namespace Http +} // namespace Filter +} // namespace Envoy diff --git a/test/common/stream_info/utility_test.cc b/test/common/stream_info/utility_test.cc index 6492488efa98..f74faa902220 100644 --- a/test/common/stream_info/utility_test.cc +++ b/test/common/stream_info/utility_test.cc @@ -15,7 +15,7 @@ namespace StreamInfo { namespace { TEST(ResponseFlagUtilsTest, toShortStringConversion) { - static_assert(ResponseFlag::LastFlag == 0x100000, "A flag has been added. Fix this code."); + static_assert(ResponseFlag::LastFlag == 0x200000, "A flag has been added. Fix this code."); std::vector> expected = { std::make_pair(ResponseFlag::FailedLocalHealthCheck, "LH"), @@ -38,7 +38,8 @@ TEST(ResponseFlagUtilsTest, toShortStringConversion) { std::make_pair(ResponseFlag::InvalidEnvoyRequestHeaders, "IH"), std::make_pair(ResponseFlag::DownstreamProtocolError, "DPE"), std::make_pair(ResponseFlag::UpstreamMaxStreamDurationReached, "UMSDR"), - std::make_pair(ResponseFlag::ResponseFromCacheFilter, "RFCF")}; + std::make_pair(ResponseFlag::ResponseFromCacheFilter, "RFCF"), + std::make_pair(ResponseFlag::NoFilterConfigFound, "NFCF")}; for (const auto& test_case : expected) { NiceMock stream_info; @@ -66,7 +67,7 @@ TEST(ResponseFlagUtilsTest, toShortStringConversion) { } TEST(ResponseFlagsUtilsTest, toResponseFlagConversion) { - static_assert(ResponseFlag::LastFlag == 0x100000, "A flag has been added. Fix this code."); + static_assert(ResponseFlag::LastFlag == 0x200000, "A flag has been added. Fix this code."); std::vector> expected = { std::make_pair("LH", ResponseFlag::FailedLocalHealthCheck), @@ -89,7 +90,8 @@ TEST(ResponseFlagsUtilsTest, toResponseFlagConversion) { std::make_pair("IH", ResponseFlag::InvalidEnvoyRequestHeaders), std::make_pair("DPE", ResponseFlag::DownstreamProtocolError), std::make_pair("UMSDR", ResponseFlag::UpstreamMaxStreamDurationReached), - std::make_pair("RFCF", ResponseFlag::ResponseFromCacheFilter)}; + std::make_pair("RFCF", ResponseFlag::ResponseFromCacheFilter), + std::make_pair("NFCF", ResponseFlag::NoFilterConfigFound)}; EXPECT_FALSE(ResponseFlagUtils::toResponseFlag("NonExistentFlag").has_value()); diff --git a/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc b/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc index b824aeb2a3ad..5e3a4460e6bf 100644 --- a/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc +++ b/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc @@ -43,6 +43,7 @@ TEST(UtilityResponseFlagsToAccessLogResponseFlagsTest, All) { common_access_log_expected.mutable_response_flags()->set_upstream_max_stream_duration_reached( true); common_access_log_expected.mutable_response_flags()->set_response_from_cache_filter(true); + common_access_log_expected.mutable_response_flags()->set_no_filter_config_found(true); EXPECT_EQ(common_access_log_expected.DebugString(), common_access_log.DebugString()); } diff --git a/test/extensions/filters/network/http_connection_manager/BUILD b/test/extensions/filters/network/http_connection_manager/BUILD index 4f264b13a029..15a050c21711 100644 --- a/test/extensions/filters/network/http_connection_manager/BUILD +++ b/test/extensions/filters/network/http_connection_manager/BUILD @@ -25,6 +25,7 @@ envoy_extension_cc_test( ":config_cc_proto", "//source/common/buffer:buffer_lib", "//source/common/event:dispatcher_lib", + "//source/common/filter/http:filter_config_discovery_lib", "//source/extensions/access_loggers/file:config", "//source/extensions/filters/http/health_check:config", "//source/extensions/filters/http/router:config", diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index 5e7648ba1ce4..170246b40eb9 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -8,6 +8,7 @@ #include "envoy/type/v3/percent.pb.h" #include "common/buffer/buffer_impl.h" +#include "common/filter/http/filter_config_discovery_impl.h" #include "common/http/date_provider_impl.h" #include "common/http/request_id_extension_uuid_impl.h" @@ -54,12 +55,14 @@ class HttpConnectionManagerConfigTest : public testing::Test { NiceMock route_config_provider_manager_; NiceMock scoped_routes_config_provider_manager_; NiceMock http_tracer_manager_; + Filter::Http::FilterConfigProviderManagerImpl filter_config_provider_manager_; std::shared_ptr> http_tracer_{ std::make_shared>()}; void createHttpConnectionManagerConfig(const std::string& yaml) { HttpConnectionManagerConfig(parseHttpConnectionManagerFromYaml(yaml), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); } }; @@ -176,7 +179,8 @@ stat_prefix: router HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(128, config.tracingConfig()->max_path_tag_length_); EXPECT_EQ(*context_.local_info_.address_, config.localAddress()); @@ -211,7 +215,8 @@ stat_prefix: router HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); // By default, tracer must be a null object (Tracing::HttpNullTracer) rather than nullptr. EXPECT_THAT(config.tracer().get(), WhenDynamicCastTo(NotNull())); @@ -249,7 +254,8 @@ stat_prefix: router HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); // Even though tracer provider is configured in the bootstrap config, a given filter instance // should not have a tracer associated with it. @@ -284,7 +290,8 @@ tracing: {} # notice that tracing is enabled HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); // Actual HttpTracer must be obtained from the HttpTracerManager. EXPECT_THAT(config.tracer(), Eq(http_tracer_)); @@ -324,7 +331,8 @@ tracing: {} # notice that tracing is enabled HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); // Actual HttpTracer must be obtained from the HttpTracerManager. EXPECT_THAT(config.tracer(), Eq(http_tracer_)); @@ -383,7 +391,8 @@ stat_prefix: router HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); // Actual HttpTracer must be obtained from the HttpTracerManager. EXPECT_THAT(config.tracer(), Eq(http_tracer_)); @@ -414,7 +423,8 @@ stat_prefix: router )EOF"; HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); std::vector custom_tags{"ltag", "etag", "rtag", "mtag"}; const Tracing::CustomTagMap& custom_tag_map = config.tracingConfig()->custom_tags_; @@ -434,7 +444,8 @@ stat_prefix: router )EOF"; HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); const Tracing::CustomTagMap& custom_tag_map = config.tracingConfig()->custom_tags_; const Tracing::RequestHeaderCustomTag* foo = dynamic_cast( @@ -466,7 +477,8 @@ stat_prefix: router ON_CALL(context_, direction()).WillByDefault(Return(envoy::config::core::v3::OUTBOUND)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(Tracing::OperationName::Egress, config.tracingConfig()->operation_name_); } @@ -492,7 +504,8 @@ stat_prefix: router ON_CALL(context_, direction()).WillByDefault(Return(envoy::config::core::v3::INBOUND)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(Tracing::OperationName::Ingress, config.tracingConfig()->operation_name_); } @@ -511,7 +524,8 @@ TEST_F(HttpConnectionManagerConfigTest, SamplingDefault) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(100, config.tracingConfig()->client_sampling_.numerator()); EXPECT_EQ(Tracing::DefaultMaxPathTagLength, config.tracingConfig()->max_path_tag_length_); @@ -546,7 +560,8 @@ TEST_F(HttpConnectionManagerConfigTest, SamplingConfigured) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(1, config.tracingConfig()->client_sampling_.numerator()); EXPECT_EQ(envoy::type::v3::FractionalPercent::HUNDRED, @@ -580,7 +595,8 @@ TEST_F(HttpConnectionManagerConfigTest, FractionalSamplingConfigured) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(0, config.tracingConfig()->client_sampling_.numerator()); EXPECT_EQ(envoy::type::v3::FractionalPercent::HUNDRED, @@ -606,7 +622,8 @@ TEST_F(HttpConnectionManagerConfigTest, UnixSocketInternalAddress) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); Network::Address::PipeInstance unixAddress{"/foo"}; Network::Address::Ipv4Instance internalIpAddress{"127.0.0.1", 0}; Network::Address::Ipv4Instance externalIpAddress{"12.0.0.1", 0}; @@ -626,7 +643,8 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbDefault) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(60, config.maxRequestHeadersKb()); } @@ -642,7 +660,8 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbConfigured) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(16, config.maxRequestHeadersKb()); } @@ -658,7 +677,8 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbMaxConfigurable) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(96, config.maxRequestHeadersKb()); } @@ -675,7 +695,8 @@ TEST_F(HttpConnectionManagerConfigTest, DisabledStreamIdleTimeout) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(0, config.streamIdleTimeout().count()); } @@ -692,7 +713,8 @@ TEST_F(HttpConnectionManagerConfigTest, DEPRECATED_FEATURE_TEST(IdleTimeout)) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string, false), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(1000, config.idleTimeout().value().count()); } @@ -710,7 +732,8 @@ TEST_F(HttpConnectionManagerConfigTest, CommonHttpProtocolIdleTimeout) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(1000, config.idleTimeout().value().count()); } @@ -726,7 +749,8 @@ TEST_F(HttpConnectionManagerConfigTest, CommonHttpProtocolIdleTimeoutDefault) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(std::chrono::hours(1), config.idleTimeout().value()); } @@ -744,7 +768,8 @@ TEST_F(HttpConnectionManagerConfigTest, CommonHttpProtocolIdleTimeoutOff) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.idleTimeout().has_value()); } @@ -760,7 +785,8 @@ TEST_F(HttpConnectionManagerConfigTest, DefaultMaxRequestHeaderCount) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(100, config.maxRequestHeadersCount()); } @@ -778,7 +804,8 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeaderCountConfigurable) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(200, config.maxRequestHeadersCount()); } @@ -797,7 +824,8 @@ TEST_F(HttpConnectionManagerConfigTest, ServerOverwrite) { &Runtime::MockSnapshot::featureEnabledDefault)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(HttpConnectionManagerConfig::HttpConnectionManagerProto::OVERWRITE, config.serverHeaderTransformation()); } @@ -817,7 +845,8 @@ TEST_F(HttpConnectionManagerConfigTest, ServerAppendIfAbsent) { &Runtime::MockSnapshot::featureEnabledDefault)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(HttpConnectionManagerConfig::HttpConnectionManagerProto::APPEND_IF_ABSENT, config.serverHeaderTransformation()); } @@ -837,7 +866,8 @@ TEST_F(HttpConnectionManagerConfigTest, ServerPassThrough) { &Runtime::MockSnapshot::featureEnabledDefault)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(HttpConnectionManagerConfig::HttpConnectionManagerProto::PASS_THROUGH, config.serverHeaderTransformation()); } @@ -858,7 +888,8 @@ TEST_F(HttpConnectionManagerConfigTest, NormalizePathDefault) { &Runtime::MockSnapshot::featureEnabledDefault)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); #ifdef ENVOY_NORMALIZE_PATH_BY_DEFAULT EXPECT_TRUE(config.shouldNormalizePath()); #else @@ -881,7 +912,8 @@ TEST_F(HttpConnectionManagerConfigTest, NormalizePathRuntime) { .WillOnce(Return(true)); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_TRUE(config.shouldNormalizePath()); } @@ -901,7 +933,8 @@ TEST_F(HttpConnectionManagerConfigTest, NormalizePathTrue) { .Times(0); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_TRUE(config.shouldNormalizePath()); } @@ -921,7 +954,8 @@ TEST_F(HttpConnectionManagerConfigTest, NormalizePathFalse) { .Times(0); HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.shouldNormalizePath()); } @@ -937,7 +971,8 @@ TEST_F(HttpConnectionManagerConfigTest, MergeSlashesDefault) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.shouldMergeSlashes()); } @@ -954,7 +989,8 @@ TEST_F(HttpConnectionManagerConfigTest, MergeSlashesTrue) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_TRUE(config.shouldMergeSlashes()); } @@ -971,7 +1007,8 @@ TEST_F(HttpConnectionManagerConfigTest, MergeSlashesFalse) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.shouldMergeSlashes()); } @@ -987,7 +1024,8 @@ TEST_F(HttpConnectionManagerConfigTest, RemovePortDefault) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.shouldStripMatchingPort()); } @@ -1004,7 +1042,8 @@ TEST_F(HttpConnectionManagerConfigTest, RemovePortTrue) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_TRUE(config.shouldStripMatchingPort()); } @@ -1021,7 +1060,8 @@ TEST_F(HttpConnectionManagerConfigTest, RemovePortFalse) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.shouldStripMatchingPort()); } @@ -1037,7 +1077,8 @@ TEST_F(HttpConnectionManagerConfigTest, HeadersWithUnderscoresAllowedByDefault) HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(envoy::config::core::v3::HttpProtocolOptions::ALLOW, config.headersWithUnderscoresAction()); } @@ -1056,7 +1097,8 @@ TEST_F(HttpConnectionManagerConfigTest, HeadersWithUnderscoresDroppedByConfig) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(envoy::config::core::v3::HttpProtocolOptions::DROP_HEADER, config.headersWithUnderscoresAction()); } @@ -1075,7 +1117,8 @@ TEST_F(HttpConnectionManagerConfigTest, HeadersWithUnderscoresRequestRejectedByC HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(envoy::config::core::v3::HttpProtocolOptions::REJECT_REQUEST, config.headersWithUnderscoresAction()); } @@ -1092,7 +1135,8 @@ TEST_F(HttpConnectionManagerConfigTest, ConfiguredRequestTimeout) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(53 * 1000, config.requestTimeout().count()); } @@ -1108,7 +1152,8 @@ TEST_F(HttpConnectionManagerConfigTest, DisabledRequestTimeout) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(0, config.requestTimeout().count()); } @@ -1123,7 +1168,8 @@ TEST_F(HttpConnectionManagerConfigTest, UnconfiguredRequestTimeout) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_EQ(0, config.requestTimeout().count()); } @@ -1497,7 +1543,8 @@ TEST_F(HttpConnectionManagerConfigTest, AlwaysSetRequestIdInResponseDefault) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_FALSE(config.alwaysSetRequestIdInResponse()); } @@ -1513,7 +1560,8 @@ TEST_F(HttpConnectionManagerConfigTest, AlwaysSetRequestIdInResponseConfigured) HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); EXPECT_TRUE(config.alwaysSetRequestIdInResponse()); } @@ -1578,7 +1626,8 @@ TEST_F(HttpConnectionManagerConfigTest, CustomRequestIDExtension) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); auto request_id_extension = dynamic_cast(config.requestIDExtension().get()); ASSERT_NE(nullptr, request_id_extension); @@ -1613,7 +1662,8 @@ TEST_F(HttpConnectionManagerConfigTest, DefaultRequestIDExtension) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); auto request_id_extension = dynamic_cast(config.requestIDExtension().get()); ASSERT_NE(nullptr, request_id_extension); @@ -1679,6 +1729,213 @@ stat_prefix: router http_connection_manager_factory(); } +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterWarmingNoDefault) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + apply_default_config_without_warming: true + type_urls: + - type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Error: filter config foo applied without warming but has no default config."); +} + +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterBadDefault) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + default_config: + "@type": type.googleapis.com/google.protobuf.Value + type_urls: + - type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Error: cannot find filter factory foo for default filter configuration with type URL " + "type.googleapis.com/google.protobuf.Value."); +} + +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterDefaultNotTerminal) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + default_config: + "@type": type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck + type_urls: + - type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Error: non-terminal filter named foo of type envoy.filters.http.health_check is the last " + "filter in a http filter chain."); +} + +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterDefaultTerminal) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + default_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + type_urls: + - type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +- name: envoy.filters.http.router + )EOF"; + + EXPECT_THROW_WITH_MESSAGE(createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Error: terminal filter named foo of type envoy.filters.http.router " + "must be the last filter in a http filter chain."); +} + +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterDefaultRequireTypeUrl) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + default_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + type_urls: + - type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck +- name: envoy.filters.http.router + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Error: filter config has type URL envoy.extensions.filters.http.router.v3.Router but " + "expect envoy.config.filter.http.health_check.v2.HealthCheck."); +} + +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterRequireTypeUrlMissingFactory) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + type_urls: + - type.googleapis.com/google.protobuf.Value + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Error: no factory found for a required type URL google.protobuf.Value."); +} + +TEST_F(HttpConnectionManagerConfigTest, DynamicFilterDefaultValid) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + default_config: + "@type": type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck + pass_through_mode: false + type_urls: + - type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck + apply_default_config_without_warming: true +- name: envoy.filters.http.router + )EOF"; + + createHttpConnectionManagerConfig(yaml_string); +} + class FilterChainTest : public HttpConnectionManagerConfigTest { public: const std::string basic_config_ = R"EOF( @@ -1705,7 +1962,8 @@ stat_prefix: router TEST_F(FilterChainTest, CreateFilterChain) { HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(basic_config_), context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); Http::MockFilterChainFactoryCallbacks callbacks; EXPECT_CALL(callbacks, addStreamFilter(_)); // Buffer @@ -1713,6 +1971,57 @@ TEST_F(FilterChainTest, CreateFilterChain) { config.createFilterChain(callbacks); } +TEST_F(FilterChainTest, CreateDynamicFilterChain) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +route_config: + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: cluster +http_filters: +- name: foo + config_discovery: + config_source: { ads: {} } + type_urls: + - type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck +- name: bar + config_discovery: + config_source: { ads: {} } + type_urls: + - type.googleapis.com/envoy.config.filter.http.health_check.v2.HealthCheck +- name: envoy.filters.http.router + )EOF"; + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); + + Http::MockFilterChainFactoryCallbacks callbacks; + Http::StreamDecoderFilterSharedPtr missing_config_filter; + EXPECT_CALL(callbacks, addStreamDecoderFilter(_)) + .Times(2) + .WillOnce(testing::SaveArg<0>(&missing_config_filter)) + .WillOnce(Return()); // MissingConfigFilter (only once) and router + config.createFilterChain(callbacks); + + Http::MockStreamDecoderFilterCallbacks decoder_callbacks; + NiceMock stream_info; + EXPECT_CALL(decoder_callbacks, streamInfo()).WillRepeatedly(ReturnRef(stream_info)); + EXPECT_CALL(decoder_callbacks, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)) + .WillRepeatedly(Return()); + Http::TestRequestHeaderMapImpl headers; + missing_config_filter->setDecoderFilterCallbacks(decoder_callbacks); + missing_config_filter->decodeHeaders(headers, false); + EXPECT_TRUE(stream_info.hasResponseFlag(StreamInfo::ResponseFlag::NoFilterConfigFound)); +} + // Tests where upgrades are configured on via the HCM. TEST_F(FilterChainTest, CreateUpgradeFilterChain) { auto hcm_config = parseHttpConnectionManagerFromYaml(basic_config_); @@ -1720,7 +2029,8 @@ TEST_F(FilterChainTest, CreateUpgradeFilterChain) { HttpConnectionManagerConfig config(hcm_config, context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); NiceMock callbacks; // Check the case where WebSockets are configured in the HCM, and no router @@ -1767,7 +2077,8 @@ TEST_F(FilterChainTest, CreateUpgradeFilterChainHCMDisabled) { HttpConnectionManagerConfig config(hcm_config, context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); NiceMock callbacks; // Check the case where WebSockets are off in the HCM, and no router config is present. @@ -1821,7 +2132,8 @@ TEST_F(FilterChainTest, CreateCustomUpgradeFilterChain) { HttpConnectionManagerConfig config(hcm_config, context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_); + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); { Http::MockFilterChainFactoryCallbacks callbacks; @@ -1865,7 +2177,8 @@ TEST_F(FilterChainTest, CreateCustomUpgradeFilterChainWithRouterNotLast) { EXPECT_THROW_WITH_MESSAGE( HttpConnectionManagerConfig(hcm_config, context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_), + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_), EnvoyException, "Error: terminal filter named envoy.filters.http.router of type envoy.filters.http.router " "must be the last filter in a http upgrade filter chain."); @@ -1879,7 +2192,8 @@ TEST_F(FilterChainTest, InvalidConfig) { EXPECT_THROW_WITH_MESSAGE( HttpConnectionManagerConfig(hcm_config, context_, date_provider_, route_config_provider_manager_, - scoped_routes_config_provider_manager_, http_tracer_manager_), + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_), EnvoyException, "Error: multiple upgrade configs with the same name: 'websocket'"); } diff --git a/test/integration/BUILD b/test/integration/BUILD index e89acf52eded..2009458e1852 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -877,6 +877,21 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "extension_discovery_integration_test", + srcs = ["extension_discovery_integration_test.cc"], + tags = ["fails_on_windows"], + deps = [ + ":http_integration_lib", + "//source/extensions/filters/http/rbac:config", + "//test/common/grpc:grpc_client_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/rbac/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/service/extension/v3:pkg_cc_proto", + ], +) + envoy_cc_test_library( name = "server_stats_interface", hdrs = ["server_stats.h"], diff --git a/test/integration/extension_discovery_integration_test.cc b/test/integration/extension_discovery_integration_test.cc new file mode 100644 index 000000000000..7af8be71c394 --- /dev/null +++ b/test/integration/extension_discovery_integration_test.cc @@ -0,0 +1,327 @@ +#include "envoy/extensions/filters/http/rbac/v3/rbac.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/service/extension/v3/config_discovery.pb.h" + +#include "test/common/grpc/grpc_client_integration.h" +#include "test/integration/http_integration.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +std::string denyPrivateConfig() { + return R"EOF( + rules: + action: DENY + policies: + "test": + permissions: + - url_path: { path: { prefix: "/private" } } + principals: + - any: true +)EOF"; +} + +std::string allowAllConfig() { + return R"EOF( + rules: + action: ALLOW + policies: + "test": + permissions: + - any: true + principals: + - any: true +)EOF"; +} + +std::string invalidConfig() { + return R"EOF( + rules: + action: DENY + policies: + "test": {} +)EOF"; +} + +class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, + public HttpIntegrationTest { +public: + ExtensionDiscoveryIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, ipVersion()) {} + + void addDynamicFilter(const std::string& name, bool apply_without_warming, + bool set_default_config = true) { + config_helper_.addConfigModifier( + [this, name, apply_without_warming, set_default_config]( + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + http_connection_manager) { + auto* filter = http_connection_manager.mutable_http_filters()->Add(); + filter->set_name(name); + auto* discovery = filter->mutable_config_discovery(); + discovery->add_type_urls( + "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC"); + if (set_default_config) { + const auto rbac_configuration = + TestUtility::parseYaml(R"EOF( + rules: + action: DENY + policies: + "test": + permissions: + - any: true + principals: + - any: true + )EOF"); + discovery->mutable_default_config()->PackFrom(rbac_configuration); + } + discovery->set_apply_default_config_without_warming(apply_without_warming); + 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, "ecds_cluster", getEcdsFakeUpstream().localAddress()); + // keep router the last + auto size = http_connection_manager.http_filters_size(); + http_connection_manager.mutable_http_filters()->SwapElements(size - 2, size - 1); + }); + } + + 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"); + ecds_cluster->mutable_http2_protocol_options(); + }); + // 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& + http_connection_manager) { + http_connection_manager.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_direct_response() + ->set_status(200); + }); + HttpIntegrationTest::initialize(); + } + + ~ExtensionDiscoveryIntegrationTest() override { + AssertionResult result = ecds_connection_->close(); + RELEASE_ASSERT(result, result.message()); + result = ecds_connection_->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + ecds_connection_.reset(); + } + + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + // Create the extension config discovery upstream (fake_upstreams_[1]). + fake_upstreams_.emplace_back(new FakeUpstream(0, FakeHttpConnection::Type::HTTP2, version_, + timeSystem(), enable_half_close_)); + for (auto& upstream : fake_upstreams_) { + upstream->set_allow_unexpected_disconnects(true); + } + } + + void waitXdsStream() { + auto& upstream = getEcdsFakeUpstream(); + AssertionResult result = 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(); + } + + void sendXdsResponse(const std::string& name, const std::string& version, + const std::string& rbac_config) { + envoy::service::discovery::v3::DiscoveryResponse response; + response.set_version_info(version); + response.set_type_url("type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig"); + const auto rbac_configuration = + TestUtility::parseYaml(rbac_config); + envoy::config::core::v3::TypedExtensionConfig typed_config; + typed_config.set_name(name); + typed_config.mutable_typed_config()->PackFrom(rbac_configuration); + response.add_resources()->PackFrom(typed_config); + ecds_stream_->sendGrpcMessage(response); + } + + FakeUpstream& getEcdsFakeUpstream() const { return *fake_upstreams_[1]; } + +private: + FakeHttpConnectionPtr ecds_connection_{nullptr}; + FakeStreamPtr ecds_stream_{nullptr}; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, ExtensionDiscoveryIntegrationTest, + GRPC_CLIENT_INTEGRATION_PARAMS); + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccess) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false); + initialize(); + 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()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.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); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl banned_request_headers{ + {":method", "GET"}, {":path", "/private/key"}, {":scheme", "http"}, {":authority", "host"}}; + { + auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); + } + // Update again but keep the connection. + { + sendXdsResponse("foo", "2", allowAllConfig()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_reload", + 2); + auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailWithDefault) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false); + initialize(); + 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()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_fail", 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailWithoutDefault) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false, false); + initialize(); + 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()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_fail", 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("500", response->headers().getStatusValue()); +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicWithoutWarming) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("bar", true); + initialize(); + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + registerTestServerPorts({"http"}); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + // Initial request uses the default config. + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + { + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); + } + + // Update should cause a different response. + sendXdsResponse("bar", "1", denyPrivateConfig()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.bar.config_reload", + 1); + { + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicWithoutWarmingFail) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("bar", true); + initialize(); + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + registerTestServerPorts({"http"}); + test_server_->waitUntilListenersReady(); + 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()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.bar.config_fail", 1); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicTwoSubscriptionsSameName) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("baz", true); + addDynamicFilter("baz", false); + initialize(); + 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()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.baz.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); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +} // namespace +} // namespace Envoy