Skip to content

Commit

Permalink
health_check: add header matching to health check http filter (envoyp…
Browse files Browse the repository at this point in the history
…roxy#3129)

 Implements the header matching mechanism that was added to the API in envoyproxy#3097 .

Risk Level: Low

Testing: Unit tests were added for the new configuration options.

Docs Changes: envoyproxy#3097.

Release Notes: added release note.

Signed-off-by: Matt Rice <[email protected]>
  • Loading branch information
mrice32 authored and htuch committed Apr 27, 2018
1 parent a12d556 commit 132b36c
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 10 deletions.
2 changes: 2 additions & 0 deletions DEPRECATED.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ message HealthCheck {
// must be healthy in order for the filter to return a 200.
map<string, envoy.type.Percent> 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
Expand Down
4 changes: 4 additions & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Version history
<envoy_api_field_core.HealthCheck.unhealthy_edge_interval>`, :ref:`unhealthy to healthy
<envoy_api_field_core.HealthCheck.healthy_edge_interval>` and for subsequent checks on
:ref:`unhealthy hosts <envoy_api_field_core.HealthCheck.unhealthy_interval>`.
* health check http filter: added
:ref:`generic header matching <envoy_api_field_config.filter.http.health_check.v2.HealthCheck.headers>`
to trigger health check response. Deprecated the
:ref:`endpoint option <envoy_api_field_config.filter.http.health_check.v2.HealthCheck.endpoint>`.
* listeners: added :ref:`tcp_fast_open_queue_length <envoy_api_field_Listener.tcp_fast_open_queue_length>` option.
* health check: added support for :ref:`custom health check <envoy_api_field_core.HealthCheck.custom_health_check>`.
* http: added the ability to pass DNS type Subject Alternative Names of the client certificate in the
Expand Down
2 changes: 2 additions & 0 deletions source/extensions/filters/http/health_check/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
Expand All @@ -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",
],
Expand Down
29 changes: 25 additions & 4 deletions source/extensions/filters/http/health_check/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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<std::vector<Router::ConfigUtility::HeaderData>>();

// 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");
}
Expand All @@ -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<HealthCheckFilter>(
context, pass_through_mode, cache_manager, hc_endpoint, cluster_min_healthy_percentages));
callbacks.addStreamFilter(std::make_shared<HealthCheckFilter>(context, pass_through_mode,
cache_manager, header_match_data,
cluster_min_healthy_percentages));

};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
12 changes: 9 additions & 3 deletions source/extensions/filters/http/health_check/health_check.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,16 +49,20 @@ typedef std::map<std::string, double> ClusterMinHealthyPercentages;
typedef std::shared_ptr<const ClusterMinHealthyPercentages>
ClusterMinHealthyPercentagesConstSharedPtr;

typedef std::shared_ptr<std::vector<Router::ConfigUtility::HeaderData>> 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 {}
Expand Down Expand Up @@ -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_;
};

Expand Down
1 change: 1 addition & 0 deletions test/extensions/filters/http/health_check/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
169 changes: 169 additions & 0 deletions test/extensions/filters/http/health_check/config_test.cc
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<Server::Configuration::MockFactoryContext> context;
ProtobufTypes::MessagePtr config_msg = healthCheckFilterConfig.createEmptyConfigProto();
auto config =
dynamic_cast<envoy::config::filter::http::health_check::v2::HealthCheck*>(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<Http::MockStreamDecoderFilterCallbacks> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::vector<Router::ConfigUtility::HeaderData>>();
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_);
}
Expand All @@ -57,6 +62,7 @@ class HealthCheckFilterTest : public testing::Test {
NiceMock<Http::MockStreamDecoderFilterCallbacks> callbacks_;
Http::TestHeaderMapImpl request_headers_;
Http::TestHeaderMapImpl request_headers_no_hc_;
HeaderDataVectorSharedPtr header_data_;

class MockHealthCheckCluster : public NiceMock<Upstream::MockThreadLocalCluster> {
public:
Expand Down

0 comments on commit 132b36c

Please sign in to comment.