diff --git a/source/extensions/filters/http/fault/config.cc b/source/extensions/filters/http/fault/config.cc index 1ee045228db1..1cd8e3a9c6a2 100644 --- a/source/extensions/filters/http/fault/config.cc +++ b/source/extensions/filters/http/fault/config.cc @@ -42,6 +42,14 @@ FaultFilterFactory::createFilterFactoryFromProto(const Protobuf::Message& proto_ stats_prefix, context); } +Router::RouteSpecificFilterConfigConstSharedPtr +FaultFilterFactory::createRouteSpecificFilterConfig(const ProtobufWkt::Struct& struct_config) { + envoy::config::filter::http::fault::v2::HTTPFault proto_config; + MessageUtil::jsonConvert(struct_config, proto_config); + return std::make_shared( + Fault::FaultSettings(proto_config)); +} + /** * Static registration for the fault filter. @see RegisterFactory. */ diff --git a/source/extensions/filters/http/fault/config.h b/source/extensions/filters/http/fault/config.h index 29253bacae19..bd5a93fdf618 100644 --- a/source/extensions/filters/http/fault/config.h +++ b/source/extensions/filters/http/fault/config.h @@ -27,6 +27,9 @@ class FaultFilterFactory : public Server::Configuration::NamedHttpFilterConfigFa return ProtobufTypes::MessagePtr{new envoy::config::filter::http::fault::v2::HTTPFault()}; } + Router::RouteSpecificFilterConfigConstSharedPtr + createRouteSpecificFilterConfig(const ProtobufWkt::Struct&) override; + std::string name() override { return HttpFilterNames::get().FAULT; } private: diff --git a/source/extensions/filters/http/fault/fault_filter.cc b/source/extensions/filters/http/fault/fault_filter.cc index af1a17b2db9e..a3280c0eaef6 100644 --- a/source/extensions/filters/http/fault/fault_filter.cc +++ b/source/extensions/filters/http/fault/fault_filter.cc @@ -20,6 +20,8 @@ #include "common/protobuf/utility.h" #include "common/router/config_impl.h" +#include "extensions/filters/http/well_known_names.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -30,15 +32,7 @@ const std::string FaultFilter::ABORT_PERCENT_KEY = "fault.http.abort.abort_perce const std::string FaultFilter::DELAY_DURATION_KEY = "fault.http.delay.fixed_duration_ms"; const std::string FaultFilter::ABORT_HTTP_STATUS_KEY = "fault.http.abort.http_status"; -FaultFilterConfig::FaultFilterConfig(const envoy::config::filter::http::fault::v2::HTTPFault& fault, - Runtime::Loader& runtime, const std::string& stats_prefix, - Stats::Scope& scope) - : runtime_(runtime), stats_(generateStats(stats_prefix, scope)), stats_prefix_(stats_prefix), - scope_(scope) { - - if (!fault.has_abort() && !fault.has_delay()) { - throw EnvoyException("fault filter must have at least abort or delay specified in the config."); - } +FaultSettings::FaultSettings(const envoy::config::filter::http::fault::v2::HTTPFault& fault) { if (fault.has_abort()) { abort_percent_ = fault.abort().percent(); @@ -62,6 +56,12 @@ FaultFilterConfig::FaultFilterConfig(const envoy::config::filter::http::fault::v } } +FaultFilterConfig::FaultFilterConfig(const envoy::config::filter::http::fault::v2::HTTPFault& fault, + Runtime::Loader& runtime, const std::string& stats_prefix, + Stats::Scope& scope) + : settings_(fault), runtime_(runtime), stats_(generateStats(stats_prefix, scope)), + stats_prefix_(stats_prefix), scope_(scope) {} + FaultFilter::FaultFilter(FaultFilterConfigSharedPtr config) : config_(config) {} FaultFilter::~FaultFilter() { ASSERT(!delay_timer_); } @@ -71,6 +71,21 @@ FaultFilter::~FaultFilter() { ASSERT(!delay_timer_); } // if we inject a delay, then we will inject the abort in the delay timer // callback. Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::HeaderMap& headers, bool) { + // Route-level configuration overrides filter-level configuration + // NOTE: We should not use runtime when reading from route-level + // faults. In other words, runtime is supported only when faults are + // configured at the filter level. + fault_settings_ = config_->settings(); + if (callbacks_->route() && callbacks_->route()->routeEntry()) { + const std::string& name = Extensions::HttpFilters::HttpFilterNames::get().FAULT; + const auto* route_entry = callbacks_->route()->routeEntry(); + + const FaultSettings* per_route_settings_ = + route_entry->perFilterConfigTyped(name) + ?: route_entry->virtualHost().perFilterConfigTyped(name); + fault_settings_ = per_route_settings_ ?: fault_settings_; + } + if (!matchesTargetUpstreamCluster()) { return Http::FilterHeadersStatus::Continue; } @@ -80,7 +95,7 @@ Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::HeaderMap& headers, b } // Check for header matches - if (!Router::ConfigUtility::matchHeaders(headers, config_->filterHeaders())) { + if (!Router::ConfigUtility::matchHeaders(headers, fault_settings_->filterHeaders())) { return Http::FilterHeadersStatus::Continue; } @@ -115,24 +130,24 @@ Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::HeaderMap& headers, b } bool FaultFilter::isDelayEnabled() { - bool enabled = - config_->runtime().snapshot().featureEnabled(DELAY_PERCENT_KEY, config_->delayPercent()); + bool enabled = config_->runtime().snapshot().featureEnabled(DELAY_PERCENT_KEY, + fault_settings_->delayPercent()); if (!downstream_cluster_delay_percent_key_.empty()) { enabled |= config_->runtime().snapshot().featureEnabled(downstream_cluster_delay_percent_key_, - config_->delayPercent()); + fault_settings_->delayPercent()); } return enabled; } bool FaultFilter::isAbortEnabled() { - bool enabled = - config_->runtime().snapshot().featureEnabled(ABORT_PERCENT_KEY, config_->abortPercent()); + bool enabled = config_->runtime().snapshot().featureEnabled(ABORT_PERCENT_KEY, + fault_settings_->abortPercent()); if (!downstream_cluster_abort_percent_key_.empty()) { enabled |= config_->runtime().snapshot().featureEnabled(downstream_cluster_abort_percent_key_, - config_->abortPercent()); + fault_settings_->abortPercent()); } return enabled; @@ -145,8 +160,8 @@ absl::optional FaultFilter::delayDuration() { return ret; } - uint64_t duration = - config_->runtime().snapshot().getInteger(DELAY_DURATION_KEY, config_->delayDuration()); + uint64_t duration = config_->runtime().snapshot().getInteger(DELAY_DURATION_KEY, + fault_settings_->delayDuration()); if (!downstream_cluster_delay_duration_key_.empty()) { duration = config_->runtime().snapshot().getInteger(downstream_cluster_delay_duration_key_, duration); @@ -163,7 +178,7 @@ absl::optional FaultFilter::delayDuration() { uint64_t FaultFilter::abortHttpStatus() { // TODO(mattklein123): check http status codes obtained from runtime. uint64_t http_status = - config_->runtime().snapshot().getInteger(ABORT_HTTP_STATUS_KEY, config_->abortCode()); + config_->runtime().snapshot().getInteger(ABORT_HTTP_STATUS_KEY, fault_settings_->abortCode()); if (!downstream_cluster_abort_http_status_key_.empty()) { http_status = config_->runtime().snapshot().getInteger( @@ -244,17 +259,17 @@ void FaultFilter::abortWithHTTPStatus() { bool FaultFilter::matchesTargetUpstreamCluster() { bool matches = true; - if (!config_->upstreamCluster().empty()) { + if (!fault_settings_->upstreamCluster().empty()) { Router::RouteConstSharedPtr route = callbacks_->route(); matches = route && route->routeEntry() && - (route->routeEntry()->clusterName() == config_->upstreamCluster()); + (route->routeEntry()->clusterName() == fault_settings_->upstreamCluster()); } return matches; } bool FaultFilter::matchesDownstreamNodes(const Http::HeaderMap& headers) { - if (config_->downstreamNodes().empty()) { + if (fault_settings_->downstreamNodes().empty()) { return true; } @@ -263,7 +278,8 @@ bool FaultFilter::matchesDownstreamNodes(const Http::HeaderMap& headers) { } const std::string downstream_node = headers.EnvoyDownstreamServiceNode()->value().c_str(); - return config_->downstreamNodes().find(downstream_node) != config_->downstreamNodes().end(); + return fault_settings_->downstreamNodes().find(downstream_node) != + fault_settings_->downstreamNodes().end(); } void FaultFilter::resetTimerState() { diff --git a/source/extensions/filters/http/fault/fault_filter.h b/source/extensions/filters/http/fault/fault_filter.h index cd64aff71b42..295e3e292964 100644 --- a/source/extensions/filters/http/fault/fault_filter.h +++ b/source/extensions/filters/http/fault/fault_filter.h @@ -35,6 +35,33 @@ struct FaultFilterStats { ALL_FAULT_FILTER_STATS(GENERATE_COUNTER_STRUCT) }; +/** + * Configuration for fault injection. + */ +class FaultSettings : public Router::RouteSpecificFilterConfig { +public: + FaultSettings(const envoy::config::filter::http::fault::v2::HTTPFault& fault); + + const std::vector& filterHeaders() const { + return fault_filter_headers_; + } + uint64_t abortPercent() const { return abort_percent_; } + uint64_t delayPercent() const { return fixed_delay_percent_; } + uint64_t delayDuration() const { return fixed_duration_ms_; } + uint64_t abortCode() const { return http_status_; } + const std::string& upstreamCluster() const { return upstream_cluster_; } + const std::unordered_set& downstreamNodes() const { return downstream_nodes_; } + +private: + uint64_t abort_percent_{}; // 0-100 + uint64_t http_status_{}; // HTTP or gRPC return codes + uint64_t fixed_delay_percent_{}; // 0-100 + uint64_t fixed_duration_ms_{}; // in milliseconds + std::string upstream_cluster_; // restrict faults to specific upstream cluster + std::vector fault_filter_headers_; + std::unordered_set downstream_nodes_{}; // Inject failures for specific downstream +}; + /** * Configuration for the fault filter. */ @@ -43,31 +70,16 @@ class FaultFilterConfig { FaultFilterConfig(const envoy::config::filter::http::fault::v2::HTTPFault& fault, Runtime::Loader& runtime, const std::string& stats_prefix, Stats::Scope& scope); - const std::vector& filterHeaders() { - return fault_filter_headers_; - } - uint64_t abortPercent() { return abort_percent_; } - uint64_t delayPercent() { return fixed_delay_percent_; } - uint64_t delayDuration() { return fixed_duration_ms_; } - uint64_t abortCode() { return http_status_; } - const std::string& upstreamCluster() { return upstream_cluster_; } Runtime::Loader& runtime() { return runtime_; } FaultFilterStats& stats() { return stats_; } - const std::unordered_set& downstreamNodes() { return downstream_nodes_; } const std::string& statsPrefix() { return stats_prefix_; } Stats::Scope& scope() { return scope_; } + const FaultSettings* settings() { return &settings_; } private: static FaultFilterStats generateStats(const std::string& prefix, Stats::Scope& scope); - uint64_t abort_percent_{}; // 0-100 - uint64_t http_status_{}; // HTTP or gRPC return codes - uint64_t fixed_delay_percent_{}; // 0-100 - uint64_t fixed_duration_ms_{}; // in milliseconds - std::string upstream_cluster_; // restrict faults to specific upstream cluster - std::vector fault_filter_headers_; - std::unordered_set downstream_nodes_{}; // Inject failures for specific downstream - // nodes. If not set then inject for all. + const FaultSettings settings_; Runtime::Loader& runtime_; FaultFilterStats stats_; const std::string stats_prefix_; @@ -112,6 +124,7 @@ class FaultFilter : public Http::StreamDecoderFilter { Event::TimerPtr delay_timer_; std::string downstream_cluster_{}; bool stream_destroyed_{}; + const FaultSettings* fault_settings_; std::string downstream_cluster_delay_percent_key_{}; std::string downstream_cluster_abort_percent_key_{}; diff --git a/test/extensions/filters/http/fault/config_test.cc b/test/extensions/filters/http/fault/config_test.cc index ebe7df44d114..3e1df4ca02bb 100644 --- a/test/extensions/filters/http/fault/config_test.cc +++ b/test/extensions/filters/http/fault/config_test.cc @@ -56,19 +56,14 @@ TEST(FaultFilterConfigTest, FaultFilterCorrectProto) { cb(filter_callback); } -TEST(FaultFilterConfigTest, InvalidFaultFilterInProto) { - envoy::config::filter::http::fault::v2::HTTPFault config{}; - NiceMock context; - FaultFilterFactory factory; - EXPECT_THROW(factory.createFilterFactoryFromProto(config, "stats", context), EnvoyException); -} - TEST(FaultFilterConfigTest, FaultFilterEmptyProto) { NiceMock context; FaultFilterFactory factory; - EXPECT_THROW( - factory.createFilterFactoryFromProto(*factory.createEmptyConfigProto(), "stats", context), - EnvoyException); + Server::Configuration::HttpFilterFactoryCb cb = + factory.createFilterFactoryFromProto(*factory.createEmptyConfigProto(), "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamDecoderFilter(_)); + cb(filter_callback); } } // namespace Fault diff --git a/test/extensions/filters/http/fault/fault_filter_test.cc b/test/extensions/filters/http/fault/fault_filter_test.cc index 7cb17ab49ef1..c231a6796bd1 100644 --- a/test/extensions/filters/http/fault/fault_filter_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_test.cc @@ -13,6 +13,7 @@ #include "common/stats/stats_impl.h" #include "extensions/filters/http/fault/fault_filter.h" +#include "extensions/filters/http/well_known_names.h" #include "test/common/http/common.h" #include "test/mocks/http/mocks.h" @@ -104,7 +105,7 @@ class FaultFilterTest : public testing::Test { } )EOF"; - const std::string fault_with_target_cluster_json = R"EOF( + const std::string delay_with_upstream_cluster_json = R"EOF( { "delay" : { "type" : "fixed", @@ -115,11 +116,21 @@ class FaultFilterTest : public testing::Test { } )EOF"; - void SetUpTest(const std::string json) { + const std::string v2_empty_fault_config_json = R"EOF( + { + } + )EOF"; + + envoy::config::filter::http::fault::v2::HTTPFault + convertJsonStrToProtoConfig(const std::string json) { Json::ObjectSharedPtr config = Json::Factory::loadFromString(json); envoy::config::filter::http::fault::v2::HTTPFault fault; - Config::FilterJson::translateFaultFilter(*config, fault); + return fault; + } + + void SetUpTest(const std::string json) { + envoy::config::filter::http::fault::v2::HTTPFault fault = convertJsonStrToProtoConfig(json); config_.reset(new FaultFilterConfig(fault, runtime_, "prefix.", stats_)); filter_.reset(new FaultFilter(config_)); filter_->setDecoderFilterCallbacks(filter_callbacks_); @@ -131,6 +142,9 @@ class FaultFilterTest : public testing::Test { EXPECT_CALL(*timer_, disableTimer()); } + void TestPerFilterConfigFault(const Router::RouteSpecificFilterConfig* route_fault, + const Router::RouteSpecificFilterConfig* vhost_fault); + FaultFilterConfigSharedPtr config_; std::unique_ptr filter_; NiceMock filter_callbacks_; @@ -651,7 +665,7 @@ TEST_F(FaultFilterTest, TimerResetAfterStreamReset) { } TEST_F(FaultFilterTest, FaultWithTargetClusterMatchSuccess) { - SetUpTest(fault_with_target_cluster_json); + SetUpTest(delay_with_upstream_cluster_json); const std::string upstream_cluster("www1"); EXPECT_CALL(filter_callbacks_.route_->route_entry_, clusterName()) @@ -693,7 +707,7 @@ TEST_F(FaultFilterTest, FaultWithTargetClusterMatchSuccess) { } TEST_F(FaultFilterTest, FaultWithTargetClusterMatchFail) { - SetUpTest(fault_with_target_cluster_json); + SetUpTest(delay_with_upstream_cluster_json); const std::string upstream_cluster("mismatch"); EXPECT_CALL(filter_callbacks_.route_->route_entry_, clusterName()) @@ -716,10 +730,10 @@ TEST_F(FaultFilterTest, FaultWithTargetClusterMatchFail) { } TEST_F(FaultFilterTest, FaultWithTargetClusterNullRoute) { - SetUpTest(fault_with_target_cluster_json); + SetUpTest(delay_with_upstream_cluster_json); const std::string upstream_cluster("www1"); - EXPECT_CALL(*filter_callbacks_.route_, routeEntry()).WillOnce(Return(nullptr)); + EXPECT_CALL(*filter_callbacks_.route_, routeEntry()).WillRepeatedly(Return(nullptr)); EXPECT_CALL(runtime_.snapshot_, featureEnabled("fault.http.delay.fixed_delay_percent", _)) .Times(0); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.delay.fixed_duration_ms", _)).Times(0); @@ -737,6 +751,79 @@ TEST_F(FaultFilterTest, FaultWithTargetClusterNullRoute) { EXPECT_EQ(0UL, config_->stats().aborts_injected_.value()); } +void FaultFilterTest::TestPerFilterConfigFault( + const Router::RouteSpecificFilterConfig* route_fault, + const Router::RouteSpecificFilterConfig* vhost_fault) { + + ON_CALL(filter_callbacks_.route_->route_entry_, + perFilterConfig(Extensions::HttpFilters::HttpFilterNames::get().FAULT)) + .WillByDefault(Return(route_fault)); + ON_CALL(filter_callbacks_.route_->route_entry_.virtual_host_, + perFilterConfig(Extensions::HttpFilters::HttpFilterNames::get().FAULT)) + .WillByDefault(Return(vhost_fault)); + + const std::string upstream_cluster("www1"); + + EXPECT_CALL(filter_callbacks_.route_->route_entry_, clusterName()) + .WillOnce(ReturnRef(upstream_cluster)); + + // Delay related calls + EXPECT_CALL(runtime_.snapshot_, featureEnabled("fault.http.delay.fixed_delay_percent", 100)) + .WillOnce(Return(true)); + + EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.delay.fixed_duration_ms", 5000)) + .WillOnce(Return(5000UL)); + + SCOPED_TRACE("PerFilterConfigFault"); + expectDelayTimer(5000UL); + + EXPECT_CALL(filter_callbacks_.request_info_, + setResponseFlag(RequestInfo::ResponseFlag::DelayInjected)); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + + // Abort related calls + EXPECT_CALL(runtime_.snapshot_, featureEnabled("fault.http.abort.abort_percent", 0)) + .WillOnce(Return(false)); + + EXPECT_CALL(filter_callbacks_, continueDecoding()); + timer_->callback_(); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_)); + + EXPECT_EQ(1UL, config_->stats().delays_injected_.value()); + EXPECT_EQ(0UL, config_->stats().aborts_injected_.value()); +} + +TEST_F(FaultFilterTest, RouteFaultOverridesListenerFault) { + + Fault::FaultSettings abort_fault(convertJsonStrToProtoConfig(abort_only_json)); + Fault::FaultSettings delay_fault(convertJsonStrToProtoConfig(delay_with_upstream_cluster_json)); + + // route-level fault overrides listener-level fault + { + SetUpTest(v2_empty_fault_config_json); // This is a valid listener level fault + TestPerFilterConfigFault(&delay_fault, nullptr); + } + + // virtual-host-level fault overrides listener-level fault + { + config_->stats().aborts_injected_.reset(); + config_->stats().delays_injected_.reset(); + SetUpTest(v2_empty_fault_config_json); + TestPerFilterConfigFault(nullptr, &delay_fault); + } + + // route-level fault overrides virtual-host-level fault + { + config_->stats().aborts_injected_.reset(); + config_->stats().delays_injected_.reset(); + SetUpTest(v2_empty_fault_config_json); + TestPerFilterConfigFault(&delay_fault, &abort_fault); + } +} + } // namespace Fault } // namespace HttpFilters } // namespace Extensions