diff --git a/api/envoy/extensions/filters/http/fault/v3/fault.proto b/api/envoy/extensions/filters/http/fault/v3/fault.proto index 534a0da35b16c..d28ed28b11100 100644 --- a/api/envoy/extensions/filters/http/fault/v3/fault.proto +++ b/api/envoy/extensions/filters/http/fault/v3/fault.proto @@ -21,6 +21,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Fault Injection :ref:`configuration overview `. // [#extension: envoy.filters.http.fault] +// [#next-free-field: 6] message FaultAbort { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.fault.v2.FaultAbort"; @@ -41,6 +42,9 @@ message FaultAbort { // HTTP status code to use to abort the HTTP request. uint32 http_status = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + // gRPC status code to use to abort the gRPC request. + uint32 grpc_status = 5; + // Fault aborts are controlled via an HTTP header (if applicable). HeaderAbort header_abort = 4; } @@ -50,7 +54,7 @@ message FaultAbort { type.v3.FractionalPercent percentage = 3; } -// [#next-free-field: 14] +// [#next-free-field: 15] message HTTPFault { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.fault.v2.HTTPFault"; @@ -133,4 +137,8 @@ message HTTPFault { // The runtime key to override the :ref:`default ` // runtime. The default is: fault.http.rate_limit.response_percent string response_rate_limit_percent_runtime = 13; + + // The runtime key to override the :ref:`default ` + // runtime. The default is: fault.http.abort.grpc_status + string abort_grpc_status_runtime = 14; } diff --git a/docs/root/configuration/http/http_filters/fault_filter.rst b/docs/root/configuration/http/http_filters/fault_filter.rst index 80678714db699..f79fc4d44a7a1 100644 --- a/docs/root/configuration/http/http_filters/fault_filter.rst +++ b/docs/root/configuration/http/http_filters/fault_filter.rst @@ -42,17 +42,27 @@ x-envoy-fault-abort-request In order for the header to work, :ref:`header_abort ` needs to be set. +x-envoy-fault-abort-grpc-request + gRPC status code to abort a request with. The header value should be a non-negative integer that specifies + the gRPC status code to return in response to a request. Its value range is [0, UInt32.Max] instead of [0, 16] + to allow testing even not well-defined gRPC status codes. When this header is set, the HTTP response status code + will be set to 200. In order for the header to work, :ref:`header_abort + ` needs to be set. If both + *x-envoy-fault-abort-request* and *x-envoy-fault-abort-grpc-request* headers are set then + *x-envoy-fault-abort-grpc-request* header will be **ignored** and fault response http status code will be + set to *x-envoy-fault-abort-request* header value. + x-envoy-fault-abort-request-percentage The percentage of requests that should be failed with a status code that's defined - by the value of *x-envoy-fault-abort-request* HTTP header. The header value should be an integer - that specifies the numerator of the percentage of request to apply aborts to and must be greater - or equal to 0 and its maximum value is capped by the value of the numerator of + by the value of *x-envoy-fault-abort-request* or *x-envoy-fault-abort-grpc-request* HTTP headers. + The header value should be an integer that specifies the numerator of the percentage of request to apply aborts + to and must be greater or equal to 0 and its maximum value is capped by the value of the numerator of :ref:`percentage ` field. Percentage's denominator is equal to default percentage's denominator :ref:`percentage ` field. In order for the header to work, :ref:`header_abort ` needs to be set and - *x-envoy-fault-abort-request* HTTP header needs to be a part of a request. + either *x-envoy-fault-abort-request* or *x-envoy-fault-abort-grpc-request* HTTP header needs to be a part of the request. x-envoy-fault-delay-request The duration to delay a request by. The header value should be an integer that specifies the number @@ -144,6 +154,13 @@ fault.http.abort.http_status available regardless of whether the filter is :ref:`configured for abort `. +fault.http.abort.grpc_status + gRPC status code that will be used as the response status code of requests that will be + aborted if the headers match. Defaults to the gRPC status code specified in the config. + If this field is missing from both the runtime and the config, gRPC status code in the response + will be derived from *fault.http.abort.http_status* field. This runtime key is only available when + the filter is :ref:`configured for abort `. + fault.http.delay.fixed_delay_percent % of requests that will be delayed if the headers match. Defaults to the *delay_percent* specified in the config or 0 otherwise. This runtime key is only available when diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index fc637cc644931..3da93218ee184 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -8,6 +8,8 @@ Changes * dynamic forward proxy: added :ref:`SNI based dynamic forward proxy ` support. * fault: added support for controlling the percentage of requests that abort, delay and response rate limits faults are applied to using :ref:`HTTP headers ` to the HTTP fault filter. +* fault: added support for specifying grpc_status code in abort faults using + :ref:`HTTP header ` or abort fault configuration in HTTP fault filter. * filter: add `upstram_rq_time` stats to the GPRC stats filter. Disabled by default and can be enabled via :ref:`enable_upstream_stats `. * http: fixed a bug where the upgrade header was not cleared on responses to non-upgrade requests. diff --git a/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto b/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto index 07996a9507ff9..9bba2f134cdf2 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto @@ -21,6 +21,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Fault Injection :ref:`configuration overview `. // [#extension: envoy.filters.http.fault] +// [#next-free-field: 6] message FaultAbort { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.fault.v2.FaultAbort"; @@ -41,16 +42,19 @@ message FaultAbort { oneof error_type { option (validate.required) = true; - // Fault aborts are controlled via an HTTP header (if applicable). + // gRPC status code to use to abort the gRPC request. uint32 http_status = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + // Fault aborts are controlled via an HTTP header (if applicable). + uint32 grpc_status = 5; + // The percentage of requests/operations/connections that will be aborted with the error code // provided. HeaderAbort header_abort = 4; } } -// [#next-free-field: 14] +// [#next-free-field: 15] message HTTPFault { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.fault.v2.HTTPFault"; @@ -133,4 +137,8 @@ message HTTPFault { // The runtime key to override the :ref:`default ` // runtime. The default is: fault.http.rate_limit.response_percent string response_rate_limit_percent_runtime = 13; + + // The runtime key to override the :ref:`default ` + // runtime. The default is: fault.http.abort.grpc_status + string abort_grpc_status_runtime = 14; } diff --git a/source/extensions/filters/common/fault/fault_config.cc b/source/extensions/filters/common/fault/fault_config.cc index 7bcdd765a3eff..ebbb86e2fd95e 100644 --- a/source/extensions/filters/common/fault/fault_config.cc +++ b/source/extensions/filters/common/fault/fault_config.cc @@ -34,7 +34,13 @@ FaultAbortConfig::FaultAbortConfig( switch (abort_config.error_type_case()) { case envoy::extensions::filters::http::fault::v3::FaultAbort::ErrorTypeCase::kHttpStatus: provider_ = - std::make_unique(abort_config.http_status(), abort_config.percentage()); + std::make_unique(static_cast(abort_config.http_status()), + absl::nullopt, abort_config.percentage()); + break; + case envoy::extensions::filters::http::fault::v3::FaultAbort::ErrorTypeCase::kGrpcStatus: + provider_ = std::make_unique( + absl::nullopt, static_cast(abort_config.grpc_status()), + abort_config.percentage()); break; case envoy::extensions::filters::http::fault::v3::FaultAbort::ErrorTypeCase::kHeaderAbort: provider_ = std::make_unique(abort_config.percentage()); @@ -44,10 +50,10 @@ FaultAbortConfig::FaultAbortConfig( } } -absl::optional FaultAbortConfig::HeaderAbortProvider::statusCode( +absl::optional FaultAbortConfig::HeaderAbortProvider::httpStatusCode( const Http::RequestHeaderMap* request_headers) const { - absl::optional ret; - const auto header = request_headers->get(HeaderNames::get().AbortRequest); + absl::optional ret = absl::nullopt; + auto header = request_headers->get(Filters::Common::Fault::HeaderNames::get().AbortRequest); if (header == nullptr) { return ret; } @@ -64,6 +70,21 @@ absl::optional FaultAbortConfig::HeaderAbortProvider::statusCode( return ret; } +absl::optional FaultAbortConfig::HeaderAbortProvider::grpcStatusCode( + const Http::RequestHeaderMap* request_headers) const { + auto header = request_headers->get(Filters::Common::Fault::HeaderNames::get().AbortGrpcRequest); + if (header == nullptr) { + return absl::nullopt; + } + + uint64_t code; + if (!absl::SimpleAtoi(header->value().getStringView(), &code)) { + return absl::nullopt; + } + + return static_cast(code); +} + FaultDelayConfig::FaultDelayConfig( const envoy::extensions::filters::common::fault::v3::FaultDelay& delay_config) { switch (delay_config.fault_delay_secifier_case()) { diff --git a/source/extensions/filters/common/fault/fault_config.h b/source/extensions/filters/common/fault/fault_config.h index 2bf80a1e67d27..e253814273b34 100644 --- a/source/extensions/filters/common/fault/fault_config.h +++ b/source/extensions/filters/common/fault/fault_config.h @@ -2,6 +2,7 @@ #include "envoy/extensions/filters/common/fault/v3/fault.pb.h" #include "envoy/extensions/filters/http/fault/v3/fault.pb.h" +#include "envoy/grpc/status.h" #include "envoy/http/header_map.h" #include "envoy/type/v3/percent.pb.h" @@ -22,6 +23,7 @@ class HeaderNameValues { const Http::LowerCaseString AbortRequest{absl::StrCat(prefix(), "-fault-abort-request")}; const Http::LowerCaseString AbortRequestPercentage{ absl::StrCat(prefix(), "-fault-abort-request-percentage")}; + const Http::LowerCaseString AbortGrpcRequest{absl::StrCat(prefix(), "-fault-abort-grpc-request")}; const Http::LowerCaseString DelayRequest{absl::StrCat(prefix(), "-fault-delay-request")}; const Http::LowerCaseString DelayRequestPercentage{ absl::StrCat(prefix(), "-fault-delay-request-percentage")}; @@ -53,8 +55,12 @@ class FaultAbortConfig { public: FaultAbortConfig(const envoy::extensions::filters::http::fault::v3::FaultAbort& abort_config); - absl::optional statusCode(const Http::RequestHeaderMap* request_headers) const { - return provider_->statusCode(request_headers); + absl::optional httpStatusCode(const Http::RequestHeaderMap* request_headers) const { + return provider_->httpStatusCode(request_headers); + } + absl::optional + grpcStatusCode(const Http::RequestHeaderMap* request_headers) const { + return provider_->grpcStatusCode(request_headers); } envoy::type::v3::FractionalPercent @@ -71,22 +77,35 @@ class FaultAbortConfig { // Return the HTTP status code to use. Optionally passed HTTP headers that may contain the // HTTP status code depending on the provider implementation. virtual absl::optional - statusCode(const Http::RequestHeaderMap* request_headers) const PURE; + httpStatusCode(const Http::RequestHeaderMap* request_headers) const PURE; + + // Return the gRPC status code to use. Optionally passed an HTTP header that may contain the + // gRPC status code depending on the provider implementation. + virtual absl::optional + grpcStatusCode(const Http::RequestHeaderMap* request_headers) const PURE; + // Return what percentage of requests abort faults should be applied to. Optionally passed // HTTP headers that may contain the percentage depending on the provider implementation. virtual envoy::type::v3::FractionalPercent percentage(const Http::RequestHeaderMap* request_headers) const PURE; }; - // Delay provider that uses a fixed abort status code. + // Abort provider that uses a fixed abort status code. class FixedAbortProvider : public AbortProvider { public: - FixedAbortProvider(uint64_t status_code, const envoy::type::v3::FractionalPercent& percentage) - : status_code_(status_code), percentage_(percentage) {} + FixedAbortProvider(absl::optional http_status_code, + absl::optional grpc_status_code, + const envoy::type::v3::FractionalPercent& percentage) + : http_status_code_(http_status_code), grpc_status_code_(grpc_status_code), + percentage_(percentage) {} - // AbortProvider - absl::optional statusCode(const Http::RequestHeaderMap*) const override { - return static_cast(status_code_); + absl::optional httpStatusCode(const Http::RequestHeaderMap*) const override { + return http_status_code_; + } + + absl::optional + grpcStatusCode(const Http::RequestHeaderMap*) const override { + return grpc_status_code_; } envoy::type::v3::FractionalPercent percentage(const Http::RequestHeaderMap*) const override { @@ -94,23 +113,30 @@ class FaultAbortConfig { } private: - const uint64_t status_code_; + const absl::optional http_status_code_; + const absl::optional grpc_status_code_; const envoy::type::v3::FractionalPercent percentage_; }; // Abort provider the reads a status code from an HTTP header. - class HeaderAbortProvider : public AbortProvider, public HeaderPercentageProvider { + class HeaderAbortProvider : public AbortProvider { public: HeaderAbortProvider(const envoy::type::v3::FractionalPercent& percentage) - : HeaderPercentageProvider(HeaderNames::get().AbortRequestPercentage, percentage) {} - // AbortProvider + : header_percentage_provider_(HeaderNames::get().AbortRequestPercentage, percentage) {} + absl::optional - statusCode(const Http::RequestHeaderMap* request_headers) const override; + httpStatusCode(const Http::RequestHeaderMap* request_headers) const override; + + absl::optional + grpcStatusCode(const Http::RequestHeaderMap* request_headers) const override; envoy::type::v3::FractionalPercent percentage(const Http::RequestHeaderMap* request_headers) const override { - return HeaderPercentageProvider::percentage(request_headers); + return header_percentage_provider_.percentage(request_headers); } + + private: + HeaderPercentageProvider header_percentage_provider_; }; using AbortProviderPtr = std::unique_ptr; @@ -176,18 +202,22 @@ class FaultDelayConfig { }; // Delay provider the reads a delay from an HTTP header. - class HeaderDelayProvider : public DelayProvider, public HeaderPercentageProvider { + class HeaderDelayProvider : public DelayProvider { public: HeaderDelayProvider(const envoy::type::v3::FractionalPercent& percentage) - : HeaderPercentageProvider(HeaderNames::get().DelayRequestPercentage, percentage) {} + : header_percentage_provider_(HeaderNames::get().DelayRequestPercentage, percentage) {} + // DelayProvider absl::optional duration(const Http::RequestHeaderMap* request_headers) const override; envoy::type::v3::FractionalPercent percentage(const Http::RequestHeaderMap* request_headers) const override { - return HeaderPercentageProvider::percentage(request_headers); + return header_percentage_provider_.percentage(request_headers); } + + private: + HeaderPercentageProvider header_percentage_provider_; }; using DelayProviderPtr = std::unique_ptr; @@ -252,16 +282,20 @@ class FaultRateLimitConfig { }; // Rate limit provider that reads the rate limit from an HTTP header. - class HeaderRateLimitProvider : public RateLimitProvider, public HeaderPercentageProvider { + class HeaderRateLimitProvider : public RateLimitProvider { public: HeaderRateLimitProvider(const envoy::type::v3::FractionalPercent& percentage) - : HeaderPercentageProvider(HeaderNames::get().ThroughputResponsePercentage, percentage) {} + : header_percentage_provider_(HeaderNames::get().ThroughputResponsePercentage, percentage) { + } // RateLimitProvider absl::optional rateKbps(const Http::RequestHeaderMap* request_headers) const override; envoy::type::v3::FractionalPercent percentage(const Http::RequestHeaderMap* request_headers) const override { - return HeaderPercentageProvider::percentage(request_headers); + return header_percentage_provider_.percentage(request_headers); } + + private: + HeaderPercentageProvider header_percentage_provider_; }; using RateLimitProviderPtr = std::unique_ptr; diff --git a/source/extensions/filters/http/fault/fault_filter.cc b/source/extensions/filters/http/fault/fault_filter.cc index f3e277edfe6b7..0f7f2ad1ca8df 100644 --- a/source/extensions/filters/http/fault/fault_filter.cc +++ b/source/extensions/filters/http/fault/fault_filter.cc @@ -43,6 +43,8 @@ FaultSettings::FaultSettings(const envoy::extensions::filters::http::fault::v3:: RuntimeKeys::get().DelayDurationKey)), abort_http_status_runtime_(PROTOBUF_GET_STRING_OR_DEFAULT( fault, abort_http_status_runtime, RuntimeKeys::get().AbortHttpStatusKey)), + abort_grpc_status_runtime_(PROTOBUF_GET_STRING_OR_DEFAULT( + fault, abort_grpc_status_runtime, RuntimeKeys::get().AbortGrpcStatusKey)), max_active_faults_runtime_(PROTOBUF_GET_STRING_OR_DEFAULT( fault, max_active_faults_runtime, RuntimeKeys::get().MaxActiveFaultsKey)), response_rate_limit_percent_runtime_( @@ -149,6 +151,8 @@ Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::RequestHeaderMap& hea fmt::format("fault.http.{}.delay.fixed_duration_ms", downstream_cluster_); downstream_cluster_abort_http_status_key_ = fmt::format("fault.http.{}.abort.http_status", downstream_cluster_); + downstream_cluster_abort_grpc_status_key_ = + fmt::format("fault.http.{}.abort.grpc_status", downstream_cluster_); } maybeSetupResponseRateLimit(headers); @@ -164,9 +168,12 @@ Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::RequestHeaderMap& hea return Http::FilterHeadersStatus::StopIteration; } - const auto abort_code = abortHttpStatus(headers); - if (abort_code.has_value()) { - abortWithHTTPStatus(abort_code.value()); + absl::optional http_status; + absl::optional grpc_status; + std::tie(http_status, grpc_status) = abortStatus(headers); + + if (http_status.has_value()) { + abortWithStatus(http_status.value(), grpc_status); return Http::FilterHeadersStatus::StopIteration; } @@ -284,29 +291,64 @@ FaultFilter::delayDuration(const Http::RequestHeaderMap& request_headers) { return ret; } -absl::optional -FaultFilter::abortHttpStatus(const Http::RequestHeaderMap& request_headers) { +AbortHttpAndGrpcStatus FaultFilter::abortStatus(const Http::RequestHeaderMap& request_headers) { if (!isAbortEnabled(request_headers)) { - return absl::nullopt; + return AbortHttpAndGrpcStatus{absl::nullopt, absl::nullopt}; + } + + auto http_status = abortHttpStatus(request_headers); + // If http status code is set, then gRPC status won't be used. + if (http_status.has_value()) { + return AbortHttpAndGrpcStatus{http_status, absl::nullopt}; } + auto grpc_status = abortGrpcStatus(request_headers); + // If gRPC status code is set, then http status will be set to Http::Code::OK (200) + if (grpc_status.has_value()) { + return AbortHttpAndGrpcStatus{Http::Code::OK, grpc_status}; + } + + return AbortHttpAndGrpcStatus{absl::nullopt, absl::nullopt}; +} + +absl::optional +FaultFilter::abortHttpStatus(const Http::RequestHeaderMap& request_headers) { // See if the configured abort provider has a default status code, if not there is no abort status // code (e.g., header configuration and no/invalid header). - const auto config_abort = fault_settings_->requestAbort()->statusCode(&request_headers); - if (!config_abort.has_value()) { + auto http_status = fault_settings_->requestAbort()->httpStatusCode(&request_headers); + if (!http_status.has_value()) { return absl::nullopt; } - auto status_code = static_cast(config_abort.value()); - auto code = static_cast(config_->runtime().snapshot().getInteger( - fault_settings_->abortHttpStatusRuntime(), status_code)); + auto default_http_status_code = static_cast(http_status.value()); + auto runtime_http_status_code = config_->runtime().snapshot().getInteger( + fault_settings_->abortHttpStatusRuntime(), default_http_status_code); if (!downstream_cluster_abort_http_status_key_.empty()) { - code = static_cast(config_->runtime().snapshot().getInteger( - downstream_cluster_abort_http_status_key_, status_code)); + runtime_http_status_code = config_->runtime().snapshot().getInteger( + downstream_cluster_abort_http_status_key_, default_http_status_code); + } + + return static_cast(runtime_http_status_code); +} + +absl::optional +FaultFilter::abortGrpcStatus(const Http::RequestHeaderMap& request_headers) { + auto grpc_status = fault_settings_->requestAbort()->grpcStatusCode(&request_headers); + if (!grpc_status.has_value()) { + return absl::nullopt; } - return code; + auto default_grpc_status_code = static_cast(grpc_status.value()); + auto runtime_grpc_status_code = config_->runtime().snapshot().getInteger( + fault_settings_->abortGrpcStatusRuntime(), default_grpc_status_code); + + if (!downstream_cluster_abort_grpc_status_key_.empty()) { + runtime_grpc_status_code = config_->runtime().snapshot().getInteger( + downstream_cluster_abort_grpc_status_key_, default_grpc_status_code); + } + + return static_cast(runtime_grpc_status_code); } void FaultFilter::recordDelaysInjectedStats() { @@ -375,18 +417,22 @@ void FaultFilter::postDelayInjection(const Http::RequestHeaderMap& request_heade resetTimerState(); // Delays can be followed by aborts - const auto abort_code = abortHttpStatus(request_headers); - if (abort_code.has_value()) { - abortWithHTTPStatus(abort_code.value()); + absl::optional http_status; + absl::optional grpc_status; + std::tie(http_status, grpc_status) = abortStatus(request_headers); + + if (http_status.has_value()) { + abortWithStatus(http_status.value(), grpc_status); } else { // Continue request processing. decoder_callbacks_->continueDecoding(); } } -void FaultFilter::abortWithHTTPStatus(Http::Code abort_code) { +void FaultFilter::abortWithStatus(Http::Code http_status_code, + absl::optional grpc_status) { decoder_callbacks_->streamInfo().setResponseFlag(StreamInfo::ResponseFlag::FaultInjected); - decoder_callbacks_->sendLocalReply(abort_code, "fault filter abort", nullptr, absl::nullopt, + decoder_callbacks_->sendLocalReply(http_status_code, "fault filter abort", nullptr, grpc_status, RcDetails::get().FaultAbort); recordAbortsInjectedStats(); } diff --git a/source/extensions/filters/http/fault/fault_filter.h b/source/extensions/filters/http/fault/fault_filter.h index bdbcbd9752821..2dfed7c9167d4 100644 --- a/source/extensions/filters/http/fault/fault_filter.h +++ b/source/extensions/filters/http/fault/fault_filter.h @@ -67,6 +67,7 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { const std::string& abortPercentRuntime() const { return abort_percent_runtime_; } const std::string& delayPercentRuntime() const { return delay_percent_runtime_; } const std::string& abortHttpStatusRuntime() const { return abort_http_status_runtime_; } + const std::string& abortGrpcStatusRuntime() const { return abort_grpc_status_runtime_; } const std::string& delayDurationRuntime() const { return delay_duration_runtime_; } const std::string& maxActiveFaultsRuntime() const { return max_active_faults_runtime_; } const std::string& responseRateLimitPercentRuntime() const { @@ -80,6 +81,7 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { const std::string AbortPercentKey = "fault.http.abort.abort_percent"; const std::string DelayDurationKey = "fault.http.delay.fixed_duration_ms"; const std::string AbortHttpStatusKey = "fault.http.abort.http_status"; + const std::string AbortGrpcStatusKey = "fault.http.abort.grpc_status"; const std::string MaxActiveFaultsKey = "fault.http.max_active_faults"; const std::string ResponseRateLimitPercentKey = "fault.http.rate_limit.response_percent"; }; @@ -98,6 +100,7 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { const std::string abort_percent_runtime_; const std::string delay_duration_runtime_; const std::string abort_http_status_runtime_; + const std::string abort_grpc_status_runtime_; const std::string max_active_faults_runtime_; const std::string response_rate_limit_percent_runtime_; }; @@ -203,6 +206,8 @@ class StreamRateLimiter : Logger::Loggable { Buffer::WatermarkBuffer buffer_; }; +using AbortHttpAndGrpcStatus = + std::pair, absl::optional>; /** * A filter that is capable of faulting an entire request before dispatching it upstream. */ @@ -245,7 +250,8 @@ class FaultFilter : public Http::StreamFilter, Logger::Loggable grpc_status_code); bool matchesTargetUpstreamCluster(); bool matchesDownstreamNodes(const Http::RequestHeaderMap& headers); bool isAbortEnabled(const Http::RequestHeaderMap& request_headers); @@ -253,7 +259,10 @@ class FaultFilter : public Http::StreamFilter, Logger::Loggable delayDuration(const Http::RequestHeaderMap& request_headers); + AbortHttpAndGrpcStatus abortStatus(const Http::RequestHeaderMap& request_headers); absl::optional abortHttpStatus(const Http::RequestHeaderMap& request_headers); + absl::optional + abortGrpcStatus(const Http::RequestHeaderMap& request_headers); void maybeIncActiveFaults(); void maybeSetupResponseRateLimit(const Http::RequestHeaderMap& request_headers); @@ -270,6 +279,7 @@ class FaultFilter : public Http::StreamFilter, Logger::Loggable 16). + Http::TestRequestHeaderMapImpl too_high_headers{{"x-envoy-fault-abort-grpc-request", "100"}}; + EXPECT_EQ(100, config.grpcStatusCode(&too_high_headers)); } TEST(FaultConfigTest, FaultAbortPercentageHeaderConfig) { diff --git a/test/extensions/filters/http/fault/fault_filter_integration_test.cc b/test/extensions/filters/http/fault/fault_filter_integration_test.cc index 6059287a66c72..0864793d136fd 100644 --- a/test/extensions/filters/http/fault/fault_filter_integration_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_integration_test.cc @@ -46,6 +46,17 @@ name: fault percentage: numerator: 100 )EOF"; + + const std::string abort_grpc_fault_config_ = + R"EOF( +name: fault +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault + abort: + grpc_status: 5 + percentage: + numerator: 100 +)EOF"; }; // Fault integration tests that should run with all protocols, useful for testing various @@ -214,6 +225,83 @@ TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultConfigNoHeaders) { EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } +// Request abort with grpc status, controlled via header configuration. +TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultAbortGrpcConfig) { + initializeFilter(header_fault_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-envoy-fault-abort-grpc-request", "5"}, + {"content-type", "application/grpc"}}); + response->waitForEndStream(); + + EXPECT_TRUE(response->complete()); + EXPECT_THAT(response->headers(), Envoy::Http::HttpStatusIs("200")); + EXPECT_THAT(response->headers(), + HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "5")); + EXPECT_THAT(response->headers(), + HeaderValueOf(Http::Headers::get().GrpcMessage, "fault filter abort")); + EXPECT_EQ(nullptr, response->trailers()); + + EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); +} + +// Request abort with grpc status, controlled via header configuration. +TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultAbortGrpcConfig0PercentageHeader) { + initializeFilter(header_fault_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-envoy-fault-abort-grpc-request", "5"}, + {"x-envoy-fault-abort-request-percentage", "0"}, + {"content-type", "application/grpc"}}); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, true); + response->waitForEndStream(); + + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); +} + +// Request abort with grpc status, controlled via configuration. +TEST_P(FaultIntegrationTestAllProtocols, FaultAbortGrpcConfig) { + initializeFilter(abort_grpc_fault_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"content-type", "application/grpc"}}); + response->waitForEndStream(); + + EXPECT_TRUE(response->complete()); + EXPECT_THAT(response->headers(), Envoy::Http::HttpStatusIs("200")); + EXPECT_THAT(response->headers(), + HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "5")); + EXPECT_THAT(response->headers(), + HeaderValueOf(Http::Headers::get().GrpcMessage, "fault filter abort")); + EXPECT_EQ(nullptr, response->trailers()); + + EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); +} + // Fault integration tests that run with HTTP/2 only, used for fully testing trailers. class FaultIntegrationTestHttp2 : public FaultIntegrationTest {}; INSTANTIATE_TEST_SUITE_P(Protocols, FaultIntegrationTestHttp2, diff --git a/test/extensions/filters/http/fault/fault_filter_test.cc b/test/extensions/filters/http/fault/fault_filter_test.cc index ce783bdca8e53..a29c5cc858161 100644 --- a/test/extensions/filters/http/fault/fault_filter_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_test.cc @@ -326,6 +326,159 @@ TEST_F(FaultFilterTest, HeaderAbortWithHttpStatus) { EXPECT_EQ("fault_filter_abort", decoder_filter_callbacks_.details_); } +TEST_F(FaultFilterTest, AbortWithGrpcStatus) { + decoder_filter_callbacks_.is_grpc_request_ = true; + + envoy::extensions::filters::http::fault::v3::HTTPFault fault; + fault.mutable_abort()->mutable_percentage()->set_numerator(100); + fault.mutable_abort()->mutable_percentage()->set_denominator( + envoy::type::v3::FractionalPercent::HUNDRED); + fault.mutable_abort()->set_grpc_status(5); + SetUpTest(fault); + + EXPECT_CALL(runtime_.snapshot_, + getInteger("fault.http.max_active_faults", std::numeric_limits::max())) + .WillOnce(Return(std::numeric_limits::max())); + + EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0); + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::DelayInjected)) + .Times(0); + + // Abort related calls + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("fault.http.abort.abort_percent", + Matcher(Percent(100)))) + .WillOnce(Return(true)); + + EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.grpc_status", 5)) + .WillOnce(Return(5)); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, + {"content-type", "application/grpc"}, + {"grpc-status", "5"}, + {"grpc-message", "fault filter abort"}}; + EXPECT_CALL(decoder_filter_callbacks_, + encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::FaultInjected)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + Http::MetadataMap metadata_map{{"metadata", "metadata"}}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->decodeMetadata(metadata_map)); + EXPECT_EQ(1UL, config_->stats().active_faults_.value()); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + filter_->onDestroy(); + + EXPECT_EQ(0UL, config_->stats().delays_injected_.value()); + EXPECT_EQ(1UL, config_->stats().aborts_injected_.value()); + EXPECT_EQ(0UL, config_->stats().active_faults_.value()); + EXPECT_EQ("fault_filter_abort", decoder_filter_callbacks_.details_); +} + +TEST_F(FaultFilterTest, HeaderAbortWithGrpcStatus) { + decoder_filter_callbacks_.is_grpc_request_ = true; + SetUpTest(header_abort_only_yaml); + + request_headers_.addCopy("x-envoy-fault-abort-grpc-request", "5"); + + EXPECT_CALL(runtime_.snapshot_, + getInteger("fault.http.max_active_faults", std::numeric_limits::max())) + .WillOnce(Return(std::numeric_limits::max())); + + EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0); + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::DelayInjected)) + .Times(0); + + // Abort related calls + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("fault.http.abort.abort_percent", + Matcher(Percent(100)))) + .WillOnce(Return(true)); + + EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.grpc_status", 5)) + .WillOnce(Return(5)); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, + {"content-type", "application/grpc"}, + {"grpc-status", "5"}, + {"grpc-message", "fault filter abort"}}; + + EXPECT_CALL(decoder_filter_callbacks_, + encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::FaultInjected)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + Http::MetadataMap metadata_map{{"metadata", "metadata"}}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->decodeMetadata(metadata_map)); + EXPECT_EQ(1UL, config_->stats().active_faults_.value()); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + filter_->onDestroy(); + + EXPECT_EQ(0UL, config_->stats().delays_injected_.value()); + EXPECT_EQ(1UL, config_->stats().aborts_injected_.value()); + EXPECT_EQ(0UL, config_->stats().active_faults_.value()); + EXPECT_EQ("fault_filter_abort", decoder_filter_callbacks_.details_); +} + +TEST_F(FaultFilterTest, HeaderAbortWithHttpAndGrpcStatus) { + SetUpTest(header_abort_only_yaml); + + request_headers_.addCopy("x-envoy-fault-abort-request", "429"); + request_headers_.addCopy("x-envoy-fault-abort-grpc-request", "5"); + + EXPECT_CALL(runtime_.snapshot_, + getInteger("fault.http.max_active_faults", std::numeric_limits::max())) + .WillOnce(Return(std::numeric_limits::max())); + + EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0); + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::DelayInjected)) + .Times(0); + + // Abort related calls + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("fault.http.abort.abort_percent", + Matcher(Percent(100)))) + .WillOnce(Return(true)); + + EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.http_status", 429)) + .WillOnce(Return(429)); + + EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.grpc_status", 5)).Times(0); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "429"}, {"content-length", "18"}, {"content-type", "text/plain"}}; + EXPECT_CALL(decoder_filter_callbacks_, + encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + EXPECT_CALL(decoder_filter_callbacks_, encodeData(_, true)); + + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::FaultInjected)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + Http::MetadataMap metadata_map{{"metadata", "metadata"}}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->decodeMetadata(metadata_map)); + EXPECT_EQ(1UL, config_->stats().active_faults_.value()); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + filter_->onDestroy(); + + EXPECT_EQ(0UL, config_->stats().delays_injected_.value()); + EXPECT_EQ(1UL, config_->stats().aborts_injected_.value()); + EXPECT_EQ(0UL, config_->stats().active_faults_.value()); + EXPECT_EQ("fault_filter_abort", decoder_filter_callbacks_.details_); +} + TEST_F(FaultFilterTest, FixedDelayZeroDuration) { SetUpTest(fixed_delay_only_yaml); @@ -1096,6 +1249,7 @@ TEST_F(FaultFilterSettingsTest, CheckDefaultRuntimeKeys) { EXPECT_EQ("fault.http.abort.abort_percent", settings.abortPercentRuntime()); EXPECT_EQ("fault.http.delay.fixed_duration_ms", settings.delayDurationRuntime()); EXPECT_EQ("fault.http.abort.http_status", settings.abortHttpStatusRuntime()); + EXPECT_EQ("fault.http.abort.grpc_status", settings.abortGrpcStatusRuntime()); EXPECT_EQ("fault.http.max_active_faults", settings.maxActiveFaultsRuntime()); EXPECT_EQ("fault.http.rate_limit.response_percent", settings.responseRateLimitPercentRuntime()); } @@ -1105,6 +1259,7 @@ TEST_F(FaultFilterSettingsTest, CheckOverrideRuntimeKeys) { fault.set_abort_percent_runtime(std::string("fault.abort_percent_runtime")); fault.set_delay_percent_runtime(std::string("fault.delay_percent_runtime")); fault.set_abort_http_status_runtime(std::string("fault.abort_http_status_runtime")); + fault.set_abort_grpc_status_runtime(std::string("fault.abort_grpc_status_runtime")); fault.set_delay_duration_runtime(std::string("fault.delay_duration_runtime")); fault.set_max_active_faults_runtime(std::string("fault.max_active_faults_runtime")); fault.set_response_rate_limit_percent_runtime( @@ -1116,6 +1271,7 @@ TEST_F(FaultFilterSettingsTest, CheckOverrideRuntimeKeys) { EXPECT_EQ("fault.abort_percent_runtime", settings.abortPercentRuntime()); EXPECT_EQ("fault.delay_duration_runtime", settings.delayDurationRuntime()); EXPECT_EQ("fault.abort_http_status_runtime", settings.abortHttpStatusRuntime()); + EXPECT_EQ("fault.abort_grpc_status_runtime", settings.abortGrpcStatusRuntime()); EXPECT_EQ("fault.max_active_faults_runtime", settings.maxActiveFaultsRuntime()); EXPECT_EQ("fault.response_rate_limit_percent_runtime", settings.responseRateLimitPercentRuntime());