diff --git a/DEPRECATED.md b/DEPRECATED.md index 016955c70e42..021508d40a90 100644 --- a/DEPRECATED.md +++ b/DEPRECATED.md @@ -19,6 +19,8 @@ A logged warning is expected for each deprecated item that is in deprecation win deprecated. Use `custom_health_check` with name `envoy.health_checkers.redis` instead. Prior to 1.7, `redis_health_check` can be used, but warning is logged. * `SAN` is replaced by `URI` in the `x-forwarded-client-cert` header. +* The `endpoint` field in the http health check filter is deprecated in favor of the `headers` + field where one can specify HeaderMatch objects to match on. ## Version 1.6.0 (March 20, 2018) diff --git a/api/envoy/config/filter/http/health_check/v2/health_check.proto b/api/envoy/config/filter/http/health_check/v2/health_check.proto index 82fb1f7a45cb..36a7fbb2d90d 100644 --- a/api/envoy/config/filter/http/health_check/v2/health_check.proto +++ b/api/envoy/config/filter/http/health_check/v2/health_check.proto @@ -32,7 +32,6 @@ message HealthCheck { // must be healthy in order for the filter to return a 200. map cluster_min_healthy_percentages = 4; - // [#not-implemented-hide:] // Specifies a set of health check request headers to match on. The health check filter will // check a request’s headers against all the specified headers. To specify the health check // endpoint, set the ``:path`` header to match on. Note that if the diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 6a6154f2edb3..e2deb132eb8c 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -25,6 +25,10 @@ Version history `, :ref:`unhealthy to healthy ` and for subsequent checks on :ref:`unhealthy hosts `. +* health check http filter: added + :ref:`generic header matching ` + to trigger health check response. Deprecated the + :ref:`endpoint option `. * listeners: added :ref:`tcp_fast_open_queue_length ` option. * health check: added support for :ref:`custom health check `. * http: added the ability to pass DNS type Subject Alternative Names of the client certificate in the diff --git a/source/extensions/filters/http/health_check/BUILD b/source/extensions/filters/http/health_check/BUILD index 82ae8ce467e8..3ef90e2f983a 100644 --- a/source/extensions/filters/http/health_check/BUILD +++ b/source/extensions/filters/http/health_check/BUILD @@ -27,6 +27,7 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/http:utility_lib", "//source/common/protobuf:utility_lib", + "//source/common/router:config_utility_lib", "@envoy_api//envoy/config/filter/http/health_check/v2:health_check_cc", ], ) @@ -39,6 +40,7 @@ envoy_cc_library( "//include/envoy/registry", "//include/envoy/server:filter_config_interface", "//source/common/config:filter_json_lib", + "//source/common/router:config_utility_lib", "//source/extensions/filters/http:well_known_names", "//source/extensions/filters/http/health_check:health_check_lib", ], diff --git a/source/extensions/filters/http/health_check/config.cc b/source/extensions/filters/http/health_check/config.cc index b2d9aca2c2cb..98084be94805 100644 --- a/source/extensions/filters/http/health_check/config.cc +++ b/source/extensions/filters/http/health_check/config.cc @@ -3,6 +3,8 @@ #include "envoy/registry/registry.h" #include "common/config/filter_json.h" +#include "common/http/headers.h" +#include "common/router/config_utility.h" #include "extensions/filters/http/health_check/health_check.h" @@ -15,12 +17,30 @@ Server::Configuration::HttpFilterFactoryCb HealthCheckFilterConfig::createFilter const envoy::config::filter::http::health_check::v2::HealthCheck& proto_config, const std::string&, Server::Configuration::FactoryContext& context) { ASSERT(proto_config.has_pass_through_mode()); - ASSERT(!proto_config.endpoint().empty()); const bool pass_through_mode = proto_config.pass_through_mode().value(); const int64_t cache_time_ms = PROTOBUF_GET_MS_OR_DEFAULT(proto_config, cache_time, 0); const std::string hc_endpoint = proto_config.endpoint(); + auto header_match_data = std::make_shared>(); + + // TODO(mrice32): remove endpoint field at the end of the 1.7.0 deprecation cycle. + const bool endpoint_set = !proto_config.endpoint().empty(); + if (endpoint_set) { + envoy::api::v2::route::HeaderMatcher matcher; + matcher.set_name(Http::Headers::get().Path.get()); + matcher.set_exact_match(proto_config.endpoint()); + header_match_data->emplace_back(matcher); + } + + for (const envoy::api::v2::route::HeaderMatcher& matcher : proto_config.headers()) { + Router::ConfigUtility::HeaderData single_header_match(matcher); + // Ignore any path header matchers if the endpoint field has been set. + if (!(endpoint_set && single_header_match.name_ == Http::Headers::get().Path)) { + header_match_data->push_back(std::move(single_header_match)); + } + } + if (!pass_through_mode && cache_time_ms) { throw EnvoyException("cache_time_ms must not be set when path_through_mode is disabled"); } @@ -40,10 +60,11 @@ Server::Configuration::HttpFilterFactoryCb HealthCheckFilterConfig::createFilter cluster_min_healthy_percentages = std::move(cluster_to_percentage); } - return [&context, pass_through_mode, cache_manager, hc_endpoint, + return [&context, pass_through_mode, cache_manager, header_match_data, cluster_min_healthy_percentages](Http::FilterChainFactoryCallbacks& callbacks) -> void { - callbacks.addStreamFilter(std::make_shared( - context, pass_through_mode, cache_manager, hc_endpoint, cluster_min_healthy_percentages)); + callbacks.addStreamFilter(std::make_shared(context, pass_through_mode, + cache_manager, header_match_data, + cluster_min_healthy_percentages)); }; } diff --git a/source/extensions/filters/http/health_check/health_check.cc b/source/extensions/filters/http/health_check/health_check.cc index 2d280f52c141..0a011869c49e 100644 --- a/source/extensions/filters/http/health_check/health_check.cc +++ b/source/extensions/filters/http/health_check/health_check.cc @@ -34,7 +34,7 @@ void HealthCheckCacheManager::onTimer() { Http::FilterHeadersStatus HealthCheckFilter::decodeHeaders(Http::HeaderMap& headers, bool end_stream) { - if (headers.Path()->value() == endpoint_.c_str()) { + if (Router::ConfigUtility::matchHeaders(headers, *header_match_data_)) { health_check_request_ = true; callbacks_->requestInfo().healthCheck(true); diff --git a/source/extensions/filters/http/health_check/health_check.h b/source/extensions/filters/http/health_check/health_check.h index c15071f068be..996f62a8a44a 100644 --- a/source/extensions/filters/http/health_check/health_check.h +++ b/source/extensions/filters/http/health_check/health_check.h @@ -9,6 +9,8 @@ #include "envoy/http/filter.h" #include "envoy/server/filter_config.h" +#include "common/router/config_utility.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -47,16 +49,20 @@ typedef std::map ClusterMinHealthyPercentages; typedef std::shared_ptr ClusterMinHealthyPercentagesConstSharedPtr; +typedef std::shared_ptr> HeaderDataVectorSharedPtr; + /** * Health check responder filter. */ class HealthCheckFilter : public Http::StreamFilter { public: HealthCheckFilter(Server::Configuration::FactoryContext& context, bool pass_through_mode, - HealthCheckCacheManagerSharedPtr cache_manager, const std::string& endpoint, + HealthCheckCacheManagerSharedPtr cache_manager, + HeaderDataVectorSharedPtr header_match_data, ClusterMinHealthyPercentagesConstSharedPtr cluster_min_healthy_percentages) : context_(context), pass_through_mode_(pass_through_mode), cache_manager_(cache_manager), - endpoint_(endpoint), cluster_min_healthy_percentages_(cluster_min_healthy_percentages) {} + header_match_data_(std::move(header_match_data)), + cluster_min_healthy_percentages_(cluster_min_healthy_percentages) {} // Http::StreamFilterBase void onDestroy() override {} @@ -91,7 +97,7 @@ class HealthCheckFilter : public Http::StreamFilter { bool health_check_request_{}; bool pass_through_mode_{}; HealthCheckCacheManagerSharedPtr cache_manager_; - const std::string endpoint_; + const HeaderDataVectorSharedPtr header_match_data_; ClusterMinHealthyPercentagesConstSharedPtr cluster_min_healthy_percentages_; }; diff --git a/test/extensions/filters/http/health_check/BUILD b/test/extensions/filters/http/health_check/BUILD index fe7fda47d171..4dc9fad02f08 100644 --- a/test/extensions/filters/http/health_check/BUILD +++ b/test/extensions/filters/http/health_check/BUILD @@ -30,5 +30,6 @@ envoy_extension_cc_test( deps = [ "//source/extensions/filters/http/health_check:config", "//test/mocks/server:server_mocks", + "//test/test_common:utility_lib", ], ) diff --git a/test/extensions/filters/http/health_check/config_test.cc b/test/extensions/filters/http/health_check/config_test.cc index da2359aff659..8227e405a9ce 100644 --- a/test/extensions/filters/http/health_check/config_test.cc +++ b/test/extensions/filters/http/health_check/config_test.cc @@ -1,10 +1,12 @@ #include "extensions/filters/http/health_check/config.h" #include "test/mocks/server/mocks.h" +#include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +using testing::Invoke; using testing::_; namespace Envoy { @@ -100,6 +102,173 @@ TEST(HealthCheckFilterConfig, HealthCheckFilterWithEmptyProto) { healthCheckFilterConfig.createFilterFactoryFromProto(config, "dummy_stats_prefix", context); } +void testHealthCheckHeaderMatch( + const envoy::config::filter::http::health_check::v2::HealthCheck& input_config, + Http::TestHeaderMapImpl& input_headers, bool expect_health_check_response) { + HealthCheckFilterConfig healthCheckFilterConfig; + NiceMock context; + ProtobufTypes::MessagePtr config_msg = healthCheckFilterConfig.createEmptyConfigProto(); + auto config = + dynamic_cast(config_msg.get()); + ASSERT_NE(config, nullptr); + + *config = input_config; + + Server::Configuration::HttpFilterFactoryCb cb = + healthCheckFilterConfig.createFilterFactoryFromProto(*config, "dummy_stats_prefix", context); + + Http::MockFilterChainFactoryCallbacks filter_callbacks; + Http::StreamFilterSharedPtr health_check_filter; + EXPECT_CALL(filter_callbacks, addStreamFilter(_)) + .WillRepeatedly(Invoke([&health_check_filter](Http::StreamFilterSharedPtr filter) { + health_check_filter = filter; + })); + + cb(filter_callbacks); + ASSERT_NE(health_check_filter, nullptr); + + NiceMock decoder_callbacks; + health_check_filter->setDecoderFilterCallbacks(decoder_callbacks); + + if (expect_health_check_response) { + // Expect that the filter intercepts this request because all headers match. + Http::TestHeaderMapImpl health_check_response{{":status", "200"}}; + EXPECT_CALL(decoder_callbacks, encodeHeaders_(HeaderMapEqualRef(&health_check_response), true)); + EXPECT_EQ(health_check_filter->decodeHeaders(input_headers, true), + Http::FilterHeadersStatus::StopIteration); + } else { + EXPECT_EQ(health_check_filter->decodeHeaders(input_headers, true), + Http::FilterHeadersStatus::Continue); + } +} + +// Basic header match with two conditions should match if both conditions are satisfied. +TEST(HealthCheckFilterConfig, HealthCheckFilterHeaderMatch) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + envoy::api::v2::route::HeaderMatcher& xheader = *config.add_headers(); + xheader.set_name("x-healthcheck"); + + envoy::api::v2::route::HeaderMatcher& yheader = *config.add_headers(); + yheader.set_name("y-healthcheck"); + yheader.set_value("foo"); + + Http::TestHeaderMapImpl headers{{"x-healthcheck", "arbitrary_value"}, {"y-healthcheck", "foo"}}; + + testHealthCheckHeaderMatch(config, headers, true); +} + +// The match should fail if a single header value fails to match. +TEST(HealthCheckFilterConfig, HealthCheckFilterHeaderMatchWrongValue) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + envoy::api::v2::route::HeaderMatcher& xheader = *config.add_headers(); + xheader.set_name("x-healthcheck"); + + envoy::api::v2::route::HeaderMatcher& yheader = *config.add_headers(); + yheader.set_name("y-healthcheck"); + yheader.set_value("foo"); + + Http::TestHeaderMapImpl headers{{"x-healthcheck", "arbitrary_value"}, {"y-healthcheck", "bar"}}; + + testHealthCheckHeaderMatch(config, headers, false); +} + +// If either of the specified headers is completely missing the match should fail. +TEST(HealthCheckFilterConfig, HealthCheckFilterHeaderMatchMissingHeader) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + envoy::api::v2::route::HeaderMatcher& xheader = *config.add_headers(); + xheader.set_name("x-healthcheck"); + + envoy::api::v2::route::HeaderMatcher& yheader = *config.add_headers(); + yheader.set_name("y-healthcheck"); + yheader.set_value("foo"); + + Http::TestHeaderMapImpl headers{{"y-healthcheck", "foo"}}; + + testHealthCheckHeaderMatch(config, headers, false); +} + +// If an endpoint is specified and the path matches, it should match regardless of any :path +// conditions given in the headers field. +TEST(HealthCheckFilterConfig, HealthCheckFilterEndpoint) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + config.set_endpoint("foo"); + + envoy::api::v2::route::HeaderMatcher& header = *config.add_headers(); + header.set_name(Http::Headers::get().Path.get()); + header.set_value("bar"); + + Http::TestHeaderMapImpl headers{{Http::Headers::get().Path.get(), "foo"}}; + + testHealthCheckHeaderMatch(config, headers, true); +} + +// If an endpoint is specified and the path does not match, the filter should not match regardless +// of any :path conditions given in the headers field. +TEST(HealthCheckFilterConfig, HealthCheckFilterEndpointOverride) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + config.set_endpoint("foo"); + + envoy::api::v2::route::HeaderMatcher& header = *config.add_headers(); + header.set_name(Http::Headers::get().Path.get()); + header.set_value("bar"); + + Http::TestHeaderMapImpl headers{{Http::Headers::get().Path.get(), "bar"}}; + + testHealthCheckHeaderMatch(config, headers, false); +} + +// Conditions for the same header should match if they are both satisfied. +TEST(HealthCheckFilterConfig, HealthCheckFilterDuplicateMatch) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + envoy::api::v2::route::HeaderMatcher& header = *config.add_headers(); + header.set_name("x-healthcheck"); + header.set_value("foo"); + + envoy::api::v2::route::HeaderMatcher& dup_header = *config.add_headers(); + dup_header.set_name("x-healthcheck"); + + Http::TestHeaderMapImpl headers{{"x-healthcheck", "foo"}}; + + testHealthCheckHeaderMatch(config, headers, true); +} + +// Conditions on the same header should not match if one or more is not satisfied. +TEST(HealthCheckFilterConfig, HealthCheckFilterDuplicateNoMatch) { + envoy::config::filter::http::health_check::v2::HealthCheck config; + + config.mutable_pass_through_mode()->set_value(false); + + envoy::api::v2::route::HeaderMatcher& header = *config.add_headers(); + header.set_name("x-healthcheck"); + header.set_value("foo"); + + envoy::api::v2::route::HeaderMatcher& dup_header = *config.add_headers(); + dup_header.set_name("x-healthcheck"); + dup_header.set_value("bar"); + + Http::TestHeaderMapImpl headers{{"x-healthcheck", "foo"}}; + + testHealthCheckHeaderMatch(config, headers, false); +} + } // namespace HealthCheck } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/health_check/health_check_test.cc b/test/extensions/filters/http/health_check/health_check_test.cc index 7facb7608cff..fb25b498a4f8 100644 --- a/test/extensions/filters/http/health_check/health_check_test.cc +++ b/test/extensions/filters/http/health_check/health_check_test.cc @@ -44,7 +44,12 @@ class HealthCheckFilterTest : public testing::Test { void prepareFilter( bool pass_through, ClusterMinHealthyPercentagesConstSharedPtr cluster_min_healthy_percentages = nullptr) { - filter_.reset(new HealthCheckFilter(context_, pass_through, cache_manager_, "/healthcheck", + header_data_ = std::make_shared>(); + envoy::api::v2::route::HeaderMatcher matcher; + matcher.set_name(":path"); + matcher.set_exact_match("/healthcheck"); + header_data_->emplace_back(matcher); + filter_.reset(new HealthCheckFilter(context_, pass_through, cache_manager_, header_data_, cluster_min_healthy_percentages)); filter_->setDecoderFilterCallbacks(callbacks_); } @@ -57,6 +62,7 @@ class HealthCheckFilterTest : public testing::Test { NiceMock callbacks_; Http::TestHeaderMapImpl request_headers_; Http::TestHeaderMapImpl request_headers_no_hc_; + HeaderDataVectorSharedPtr header_data_; class MockHealthCheckCluster : public NiceMock { public: