diff --git a/source/extensions/common/aws/BUILD b/source/extensions/common/aws/BUILD index 96382e2095c20..b5d8840695006 100644 --- a/source/extensions/common/aws/BUILD +++ b/source/extensions/common/aws/BUILD @@ -40,6 +40,18 @@ envoy_cc_library( external_deps = ["abseil_optional"], ) +envoy_cc_library( + name = "metadata_fetcher_lib", + srcs = ["metadata_fetcher.cc"], + hdrs = ["metadata_fetcher.h"], + deps = [ + ":utility_lib", + "//envoy/upstream:cluster_manager_interface", + "//source/common/http:utility_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "credentials_provider_impl_lib", srcs = ["credentials_provider_impl.cc"], @@ -63,10 +75,14 @@ envoy_cc_library( external_deps = ["curl"], deps = [ "//envoy/http:message_interface", + "//envoy/upstream:cluster_manager_interface", "//source/common/common:empty_string", "//source/common/common:matchers_lib", "//source/common/common:utility_lib", "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/upstreams/http/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/common/aws/metadata_fetcher.cc b/source/extensions/common/aws/metadata_fetcher.cc new file mode 100644 index 0000000000000..339f75be7c2c4 --- /dev/null +++ b/source/extensions/common/aws/metadata_fetcher.cc @@ -0,0 +1,179 @@ +#include "source/extensions/common/aws/metadata_fetcher.h" + +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/core/v3/http_uri.pb.h" + +#include "source/common/common/enum_to_int.h" +#include "source/common/http/headers.h" +#include "source/common/http/utility.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Aws { + +namespace { + +class MetadataFetcherImpl : public MetadataFetcher, + public Logger::Loggable, + public Http::AsyncClient::Callbacks { + +public: + MetadataFetcherImpl(Upstream::ClusterManager& cm, absl::string_view cluster_name) + : cm_(cm), cluster_name_(std::string(cluster_name)) {} + + ~MetadataFetcherImpl() override { cancel(); } + + void cancel() override { + if (request_ && !complete_) { + request_->cancel(); + ENVOY_LOG(debug, "fetch AWS Metadata [cluster = {}]: cancelled", cluster_name_); + } + reset(); + } + + absl::string_view failureToString(MetadataFetcher::MetadataReceiver::Failure reason) override { + switch (reason) { + case MetadataFetcher::MetadataReceiver::Failure::Network: + return "Network"; + case MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata: + return "InvalidMetadata"; + case MetadataFetcher::MetadataReceiver::Failure::MissingConfig: + return "MissingConfig"; + default: + return ""; + } + } + + void fetch(Http::RequestMessage& message, Tracing::Span& parent_span, + MetadataFetcher::MetadataReceiver& receiver) override { + ASSERT(!request_); + ASSERT(!receiver_); + complete_ = false; + receiver_ = makeOptRef(receiver); + const auto thread_local_cluster = cm_.getThreadLocalCluster(cluster_name_); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(error, "{} AWS Metadata failed: [cluster = {}] not found", __func__, cluster_name_); + complete_ = true; + receiver_->onMetadataError(MetadataFetcher::MetadataReceiver::Failure::MissingConfig); + reset(); + return; + } + + constexpr uint64_t MAX_RETRIES = 3; + constexpr uint64_t RETRY_DELAY = 1000; + constexpr uint64_t TIMEOUT = 5 * 1000; + + const auto host_attributes = Http::Utility::parseAuthority(message.headers().getHostValue()); + const auto host = host_attributes.host_; + const auto path = message.headers().getPathValue(); + const auto scheme = message.headers().getSchemeValue(); + const auto method = message.headers().getMethodValue(); + ENVOY_LOG(debug, "fetch AWS Metadata at [uri = {}]: start from cluster {}", + fmt::format("{}://{}{}", scheme, host, path), cluster_name_); + + Http::RequestHeaderMapPtr headersPtr = + Envoy::Http::createHeaderMap( + {{Envoy::Http::Headers::get().Method, std::string(method)}, + {Envoy::Http::Headers::get().Host, std::string(host)}, + {Envoy::Http::Headers::get().Scheme, std::string(scheme)}, + {Envoy::Http::Headers::get().Path, std::string(path)}}); + + // Copy the remaining headers. + message.headers().iterate( + [&headersPtr](const Http::HeaderEntry& entry) -> Http::HeaderMap::Iterate { + // Skip pseudo-headers + if (!entry.key().getStringView().empty() && entry.key().getStringView()[0] == ':') { + return Http::HeaderMap::Iterate::Continue; + } + headersPtr->addCopy(Http::LowerCaseString(entry.key().getStringView()), + entry.value().getStringView()); + return Http::HeaderMap::Iterate::Continue; + }); + + auto messagePtr = std::make_unique(std::move(headersPtr)); + + auto options = Http::AsyncClient::RequestOptions() + .setTimeout(std::chrono::milliseconds(TIMEOUT)) + .setParentSpan(parent_span) + .setSendXff(false) + .setChildSpanName("AWS Metadata Fetch"); + + envoy::config::route::v3::RetryPolicy route_retry_policy; + route_retry_policy.mutable_num_retries()->set_value(MAX_RETRIES); + route_retry_policy.mutable_per_try_timeout()->CopyFrom( + Protobuf::util::TimeUtil::MillisecondsToDuration(TIMEOUT)); + route_retry_policy.mutable_per_try_idle_timeout()->CopyFrom( + Protobuf::util::TimeUtil::MillisecondsToDuration(RETRY_DELAY)); + route_retry_policy.set_retry_on("5xx,gateway-error,connect-failure,reset,refused-stream"); + + options.setRetryPolicy(route_retry_policy); + options.setBufferBodyForRetry(true); + request_ = makeOptRefFromPtr( + thread_local_cluster->httpAsyncClient().send(std::move(messagePtr), *this, options)); + } + + // HTTP async receive method on success. + void onSuccess(const Http::AsyncClient::Request&, Http::ResponseMessagePtr&& response) override { + complete_ = true; + const uint64_t status_code = Http::Utility::getResponseStatus(response->headers()); + if (status_code == enumToInt(Http::Code::OK)) { + ENVOY_LOG(debug, "{}: fetch AWS Metadata [cluster = {}]: success", __func__, cluster_name_); + if (response->body().length() != 0) { + const auto body = response->bodyAsString(); + receiver_->onMetadataSuccess(std::move(body)); + } else { + ENVOY_LOG(debug, "{}: fetch AWS Metadata [cluster = {}]: body is empty", __func__, + cluster_name_); + receiver_->onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + if (response->body().length() != 0) { + ENVOY_LOG(debug, "{}: fetch AWS Metadata [cluster = {}]: response status code {}, body: {}", + __func__, cluster_name_, status_code, response->bodyAsString()); + } else { + ENVOY_LOG(debug, + "{}: fetch AWS Metadata [cluster = {}]: response status code {}, body is empty", + __func__, cluster_name_, status_code); + } + receiver_->onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + reset(); + } + + // HTTP async receive method on failure. + void onFailure(const Http::AsyncClient::Request&, + Http::AsyncClient::FailureReason reason) override { + ENVOY_LOG(debug, "{}: fetch AWS Metadata [cluster = {}]: network error {}", __func__, + cluster_name_, enumToInt(reason)); + complete_ = true; + receiver_->onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + reset(); + } + + // TODO(suniltheta): Add metadata fetch status into the span like it is done on ext_authz filter. + void onBeforeFinalizeUpstreamSpan(Tracing::Span&, const Http::ResponseHeaderMap*) override {} + +private: + bool complete_{}; + Upstream::ClusterManager& cm_; + const std::string cluster_name_; + OptRef receiver_; + OptRef request_; + + void reset() { + request_.reset(); + receiver_.reset(); + } +}; +} // namespace + +MetadataFetcherPtr MetadataFetcher::create(Upstream::ClusterManager& cm, + absl::string_view cluster_name) { + return std::make_unique(cm, cluster_name); +} +} // namespace Aws +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/aws/metadata_fetcher.h b/source/extensions/common/aws/metadata_fetcher.h new file mode 100644 index 0000000000000..a39d1480447c0 --- /dev/null +++ b/source/extensions/common/aws/metadata_fetcher.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/http/message.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/http/message_impl.h" +#include "source/extensions/common/aws/utility.h" + +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Aws { + +class MetadataFetcher; +using MetadataFetcherPtr = std::unique_ptr; + +/** + * MetadataFetcher interface can be used to retrieve AWS Metadata from various providers. + * An instance of this interface is designed to retrieve one AWS Metadata at a time. + * The implementation of AWS Metadata Fetcher is similar to JwksFetcher. + */ + +class MetadataFetcher { +public: + class MetadataReceiver { + public: + enum class Failure { + /* A network error occurred causing AWS Metadata retrieval failure. */ + Network, + /* A failure occurred when trying to parse the retrieved AWS Metadata data. */ + InvalidMetadata, + /* A missing config causing AWS Metadata retrieval failure. */ + MissingConfig, + }; + + virtual ~MetadataReceiver() = default; + + /** + * @brief Successful retrieval callback of returned AWS Metadata. + * @param body Fetched AWS Metadata. + */ + virtual void onMetadataSuccess(const std::string&& body) PURE; + + /** + * @brief Retrieval error callback. + * @param reason the failure reason. + */ + virtual void onMetadataError(Failure reason) PURE; + }; + + virtual ~MetadataFetcher() = default; + + /** + * @brief Cancel any in-flight request. + */ + virtual void cancel() PURE; + + /** + * @brief Retrieve a AWS Metadata from a remote HTTP host. + * At most one outstanding request may be in-flight. + * i.e. from the invocation of `fetch()` until either + * a callback or `cancel()` is invoked, no additional + * `fetch()` may be issued. The URI to fetch is to pre + * determined based on the credentials provider source. + * + * @param receiver the receiver of the fetched AWS Metadata or error + */ + virtual void fetch(Http::RequestMessage& message, Tracing::Span& parent_span, + MetadataReceiver& receiver) PURE; + + /** + * @brief Return MetadataReceiver Failure enum as a string. + * + * @return absl::string_view + */ + virtual absl::string_view failureToString(MetadataReceiver::Failure) PURE; + + /** + * @brief Factory method for creating a Metadata Fetcher. + * + * @param cm the cluster manager to use during AWS Metadata retrieval + * @param provider the AWS Metadata provider + * @return a MetadataFetcher instance + */ + static MetadataFetcherPtr create(Upstream::ClusterManager& cm, absl::string_view cluster_name); +}; +} // namespace Aws +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/aws/utility.cc b/source/extensions/common/aws/utility.cc index 9d5669b2113a0..1643e4068ba7d 100644 --- a/source/extensions/common/aws/utility.cc +++ b/source/extensions/common/aws/utility.cc @@ -1,13 +1,18 @@ #include "source/extensions/common/aws/utility.h" +#include "envoy/upstream/cluster_manager.h" + #include "source/common/common/empty_string.h" #include "source/common/common/fmt.h" #include "source/common/common/utility.h" +#include "source/common/protobuf/message_validator_impl.h" +#include "source/common/protobuf/utility.h" #include "absl/strings/match.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "curl/curl.h" +#include "fmt/printf.h" namespace Envoy { namespace Extensions { @@ -294,6 +299,57 @@ absl::optional Utility::fetchMetadata(Http::RequestMessage& message return buffer.empty() ? absl::nullopt : absl::optional(buffer); } +bool Utility::addInternalClusterStatic( + Upstream::ClusterManager& cm, absl::string_view cluster_name, + const envoy::config::cluster::v3::Cluster::DiscoveryType cluster_type, absl::string_view uri) { + // Check if local cluster exists with that name. + if (cm.getThreadLocalCluster(cluster_name) == nullptr) { + // Make sure we run this on main thread. + TRY_ASSERT_MAIN_THREAD { + envoy::config::cluster::v3::Cluster cluster; + absl::string_view host_port; + absl::string_view path; + Http::Utility::extractHostPathFromUri(uri, host_port, path); + const auto host_attributes = Http::Utility::parseAuthority(host_port); + const auto host = host_attributes.host_; + const auto port = host_attributes.port_ ? host_attributes.port_.value() : 80; + + cluster.set_name(cluster_name); + cluster.set_type(cluster_type); + cluster.mutable_connect_timeout()->set_seconds(5); + cluster.mutable_load_assignment()->set_cluster_name(cluster_name); + auto* endpoint = cluster.mutable_load_assignment() + ->add_endpoints() + ->add_lb_endpoints() + ->mutable_endpoint(); + auto* addr = endpoint->mutable_address(); + addr->mutable_socket_address()->set_address(host); + addr->mutable_socket_address()->set_port_value(port); + cluster.set_lb_policy(envoy::config::cluster::v3::Cluster::ROUND_ROBIN); + envoy::extensions::upstreams::http::v3::HttpProtocolOptions protocol_options; + auto* http_protocol_options = + protocol_options.mutable_explicit_http_config()->mutable_http_protocol_options(); + http_protocol_options->set_accept_http_10(true); + (*cluster.mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(protocol_options); + + // TODO(suniltheta): use random number generator here for cluster version. + cm.addOrUpdateCluster(cluster, "12345"); + ENVOY_LOG_MISC(info, + "Added a {} internal cluster [name: {}, address:{}:{}] to fetch aws " + "credentials", + cluster_type, cluster_name, host, port); + } + END_TRY + CATCH(const EnvoyException& e, { + ENVOY_LOG_MISC(error, "Failed to add internal cluster {}: {}", cluster_name, e.what()); + return false; + }); + } + return true; +} + } // namespace Aws } // namespace Common } // namespace Extensions diff --git a/source/extensions/common/aws/utility.h b/source/extensions/common/aws/utility.h index 2ec7cae045cd3..985ab0de6d9f9 100644 --- a/source/extensions/common/aws/utility.h +++ b/source/extensions/common/aws/utility.h @@ -1,9 +1,13 @@ #pragma once +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/extensions/upstreams/http/v3/http_protocol_options.pb.h" +#include "envoy/extensions/upstreams/http/v3/http_protocol_options.pb.validate.h" #include "envoy/http/message.h" #include "source/common/common/matchers.h" #include "source/common/http/headers.h" +#include "source/common/http/utility.h" namespace Envoy { namespace Extensions { @@ -92,6 +96,24 @@ class Utility { * gRPC auth plugins that are able to schedule blocking plugins on a different thread. */ static absl::optional fetchMetadata(Http::RequestMessage& message); + + /** + * @brief Adds a static cluster towards a credentials provider + * to fetch the credentials using http async client. + * + * @param cm cluster manager + * @param cluster_name a name for credentials provider cluster + * @param cluster_type STATIC or STRICT_DNS or LOGICAL_DNS etc + * @param uri provider's IP (STATIC cluster) or URL (STRICT_DNS). Will use port 80 if the port is + * not specified in the uri or no matching cluster is found. + * @return true if successfully added the cluster or if a cluster with the cluster_name already + * exists. + * @return false if failed to add the cluster + */ + static bool + addInternalClusterStatic(Upstream::ClusterManager& cm, absl::string_view cluster_name, + const envoy::config::cluster::v3::Cluster::DiscoveryType cluster_type, + absl::string_view uri); }; } // namespace Aws diff --git a/test/extensions/common/aws/BUILD b/test/extensions/common/aws/BUILD index 55ebdf79f19fa..43ce091b0f550 100644 --- a/test/extensions/common/aws/BUILD +++ b/test/extensions/common/aws/BUILD @@ -14,8 +14,11 @@ envoy_cc_mock( srcs = ["mocks.cc"], hdrs = ["mocks.h"], deps = [ + "//source/common/http:message_lib", "//source/extensions/common/aws:credentials_provider_interface", + "//source/extensions/common/aws:metadata_fetcher_lib", "//source/extensions/common/aws:signer_interface", + "//test/mocks/upstream:cluster_manager_mocks", ], ) @@ -37,6 +40,7 @@ envoy_cc_test( srcs = ["utility_test.cc"], deps = [ "//source/extensions/common/aws:utility_lib", + "//test/extensions/common/aws:aws_mocks", "//test/test_common:utility_lib", ], ) @@ -50,6 +54,22 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "metadata_fetcher_test", + srcs = ["metadata_fetcher_test.cc"], + deps = [ + "//source/extensions/common/aws:metadata_fetcher_lib", + "//test/extensions/common/aws:aws_mocks", + "//test/extensions/filters/http/common:mock_lib", + "//test/mocks/api:api_mocks", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "credentials_provider_impl_test", srcs = ["credentials_provider_impl_test.cc"], diff --git a/test/extensions/common/aws/metadata_fetcher_test.cc b/test/extensions/common/aws/metadata_fetcher_test.cc new file mode 100644 index 0000000000000..d009625e952a1 --- /dev/null +++ b/test/extensions/common/aws/metadata_fetcher_test.cc @@ -0,0 +1,283 @@ +#include +#include +#include + +#include "source/common/http/headers.h" +#include "source/common/http/message_impl.h" +#include "source/common/http/utility.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/common/aws/metadata_fetcher.h" + +#include "test/extensions/common/aws/mocks.h" +#include "test/extensions/filters/http/common/mock.h" +#include "test/mocks/api/mocks.h" +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/test_common/environment.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +using Envoy::Extensions::HttpFilters::Common::MockUpstream; +using testing::_; +using testing::AllOf; +using testing::InSequence; +using testing::Mock; +using testing::NiceMock; +using testing::Ref; +using testing::Return; +using testing::Throw; +using testing::UnorderedElementsAre; + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Aws { + +MATCHER_P(OptionsHasBufferBodyForRetry, expectedValue, "") { + *result_listener << "\nexpected { buffer_body_for_retry: \"" << expectedValue + << "\"} but got {buffer_body_for_retry: \"" << arg.buffer_body_for_retry + << "\"}\n"; + return ExplainMatchResult(expectedValue, arg.buffer_body_for_retry, result_listener); +} + +MATCHER_P(NumRetries, expectedRetries, "") { + *result_listener << "\nexpected { num_retries: \"" << expectedRetries + << "\"} but got {num_retries: \"" << arg.num_retries().value() << "\"}\n"; + return ExplainMatchResult(expectedRetries, arg.num_retries().value(), result_listener); +} + +MATCHER_P(PerTryTimeout, expectedTimeout, "") { + *result_listener << "\nexpected { per_try_timeout: \"" << expectedTimeout + << "\"} but got { per_try_timeout: \"" << arg.per_try_timeout().seconds() + << "\"}\n"; + return ExplainMatchResult(expectedTimeout, arg.per_try_timeout().seconds(), result_listener); +} + +MATCHER_P(PerTryIdleTimeout, expectedIdleTimeout, "") { + *result_listener << "\nexpected { per_try_idle_timeout: \"" << expectedIdleTimeout + << "\"} but got { per_try_idle_timeout: \"" + << arg.per_try_idle_timeout().seconds() << "\"}\n"; + return ExplainMatchResult(expectedIdleTimeout, arg.per_try_idle_timeout().seconds(), + result_listener); +} + +MATCHER_P(RetryOnModes, expectedModes, "") { + const std::string& retry_on = arg.retry_on(); + std::set retry_on_modes = absl::StrSplit(retry_on, ','); + *result_listener << "\nexpected retry_on modes doesn't match " + << "received { retry_on modes: \"" << retry_on << "\"}\n"; + return ExplainMatchResult(expectedModes, retry_on_modes, result_listener); +} + +MATCHER_P(OptionsHasRetryPolicy, policyMatcher, "") { + if (!arg.retry_policy.has_value()) { + *result_listener << "Expected options to have retry policy, but it was unset"; + return false; + } + return ExplainMatchResult(policyMatcher, arg.retry_policy.value(), result_listener); +} + +class MetadataFetcherTest : public testing::Test { +public: + void setupFetcher() { + mock_factory_ctx_.cluster_manager_.initializeThreadLocalClusters({"cluster_name"}); + fetcher_ = MetadataFetcher::create(mock_factory_ctx_.cluster_manager_, "cluster_name"); + EXPECT_TRUE(fetcher_ != nullptr); + } + + testing::NiceMock mock_factory_ctx_; + std::unique_ptr fetcher_; + NiceMock parent_span_; +}; + +TEST_F(MetadataFetcherTest, TestGetSuccess) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + std::string body = "not_empty"; + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, "200", body); + MockMetadataReceiver receiver; + EXPECT_CALL(receiver, onMetadataSuccess(std::move(body))); + EXPECT_CALL(receiver, onMetadataError(_)).Times(0); + + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestRequestMatchAndSpanPassedDown) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + + message.headers().setScheme(Http::Headers::get().SchemeValues.Http); + message.headers().setMethod(Http::Headers::get().MethodValues.Get); + message.headers().setHost("169.254.170.2:80"); + message.headers().setPath("/v2/credentials/c68caeb5-ef71-4914-8170-111111111111"); + message.headers().setCopy(Http::LowerCaseString(":pseudo-header"), "peudo-header-value"); + message.headers().setCopy(Http::LowerCaseString("X-aws-ec2-metadata-token"), "Token"); + + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, "200", "not_empty"); + MockMetadataReceiver receiver; + Http::MockAsyncClientRequest httpClientRequest( + &mock_factory_ctx_.cluster_manager_.thread_local_cluster_.async_client_); + + EXPECT_CALL(mock_factory_ctx_.cluster_manager_.thread_local_cluster_.async_client_, + send_(_, _, _)) + .WillOnce(Invoke( + [this, &httpClientRequest]( + Http::RequestMessagePtr& request, Http::AsyncClient::Callbacks& cb, + const Http::AsyncClient::RequestOptions& options) -> Http::AsyncClient::Request* { + Http::TestRequestHeaderMapImpl injected_headers = { + {":method", "GET"}, + {":scheme", "http"}, + {":authority", "169.254.170.2"}, + {":path", "/v2/credentials/c68caeb5-ef71-4914-8170-111111111111"}, + {"X-aws-ec2-metadata-token", "Token"}}; + EXPECT_THAT(request->headers(), IsSupersetOfHeaders(injected_headers)); + EXPECT_TRUE(request->headers().get(Http::LowerCaseString(":pseudo-header")).empty()); + + // Verify expectations for span + EXPECT_TRUE(options.parent_span_ == &this->parent_span_); + EXPECT_TRUE(options.child_span_name_ == "AWS Metadata Fetch"); + + // Let's say this ends up with a failure then verify it is handled properly by calling + // onMetadataError. + cb.onFailure(httpClientRequest, Http::AsyncClient::FailureReason::Reset); + return &httpClientRequest; + })); + EXPECT_CALL(receiver, onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network)); + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestGet400) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, "400", "not_empty"); + MockMetadataReceiver receiver; + EXPECT_CALL(receiver, onMetadataSuccess(_)).Times(0); + EXPECT_CALL(receiver, onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network)); + + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestGet400NoBody) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, "400", ""); + MockMetadataReceiver receiver; + EXPECT_CALL(receiver, onMetadataSuccess(_)).Times(0); + EXPECT_CALL(receiver, onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network)); + + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestGetNoBody) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, "200", ""); + MockMetadataReceiver receiver; + EXPECT_CALL(receiver, onMetadataSuccess(_)).Times(0); + EXPECT_CALL(receiver, + onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata)); + + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestHttpFailure) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, + Http::AsyncClient::FailureReason::Reset); + MockMetadataReceiver receiver; + EXPECT_CALL(receiver, onMetadataSuccess(_)).Times(0); + EXPECT_CALL(receiver, onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network)); + + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestClusterNotFound) { + // Setup without thread local cluster + fetcher_ = MetadataFetcher::create(mock_factory_ctx_.cluster_manager_, "cluster_name"); + Http::RequestMessageImpl message; + MockMetadataReceiver receiver; + + EXPECT_CALL(mock_factory_ctx_.cluster_manager_, getThreadLocalCluster(_)) + .WillOnce(Return(nullptr)); + EXPECT_CALL(receiver, onMetadataSuccess(_)).Times(0); + EXPECT_CALL(receiver, onMetadataError(MetadataFetcher::MetadataReceiver::Failure::MissingConfig)); + + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestCancel) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + Http::MockAsyncClientRequest request( + &(mock_factory_ctx_.cluster_manager_.thread_local_cluster_.async_client_)); + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, &request); + MockMetadataReceiver receiver; + EXPECT_CALL(request, cancel()); + EXPECT_CALL(receiver, onMetadataSuccess(_)).Times(0); + EXPECT_CALL(receiver, onMetadataError(_)).Times(0); + + // Act + fetcher_->fetch(message, parent_span_, receiver); + // Proper cancel + fetcher_->cancel(); + Mock::VerifyAndClearExpectations(&request); + Mock::VerifyAndClearExpectations(&receiver); + // Re-entrant cancel should do nothing. + EXPECT_CALL(request, cancel()).Times(0); + fetcher_->cancel(); +} + +TEST_F(MetadataFetcherTest, TestDefaultRetryPolicy) { + // Setup + setupFetcher(); + Http::RequestMessageImpl message; + MockUpstream mock_result(mock_factory_ctx_.cluster_manager_, "200", "not_empty"); + MockMetadataReceiver receiver; + + EXPECT_CALL( + mock_factory_ctx_.cluster_manager_.thread_local_cluster_.async_client_, + send_(_, _, + AllOf(OptionsHasBufferBodyForRetry(true), + OptionsHasRetryPolicy(AllOf( + NumRetries(3), PerTryTimeout(5), PerTryIdleTimeout(1), + RetryOnModes(UnorderedElementsAre("5xx", "gateway-error", "connect-failure", + "refused-stream", "reset"))))))) + .WillOnce(Return(nullptr)); + // Act + fetcher_->fetch(message, parent_span_, receiver); +} + +TEST_F(MetadataFetcherTest, TestFailureToStringConversion) { + // Setup + setupFetcher(); + EXPECT_EQ(fetcher_->failureToString(MetadataFetcher::MetadataReceiver::Failure::Network), + "Network"); + EXPECT_EQ(fetcher_->failureToString(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata), + "InvalidMetadata"); + EXPECT_EQ(fetcher_->failureToString(MetadataFetcher::MetadataReceiver::Failure::MissingConfig), + "MissingConfig"); +} + +} // namespace Aws +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/aws/mocks.h b/test/extensions/common/aws/mocks.h index 5c3b0c7041aff..6db726a8f9362 100644 --- a/test/extensions/common/aws/mocks.h +++ b/test/extensions/common/aws/mocks.h @@ -1,8 +1,14 @@ #pragma once +#include "envoy/http/message.h" + +#include "source/common/http/message_impl.h" #include "source/extensions/common/aws/credentials_provider.h" +#include "source/extensions/common/aws/metadata_fetcher.h" #include "source/extensions/common/aws/signer.h" +#include "test/mocks/upstream/cluster_manager.h" + #include "gmock/gmock.h" namespace Envoy { @@ -10,6 +16,21 @@ namespace Extensions { namespace Common { namespace Aws { +class MockMetadataFetcher : public MetadataFetcher { +public: + MOCK_METHOD(void, cancel, ()); + MOCK_METHOD(absl::string_view, failureToString, (MetadataFetcher::MetadataReceiver::Failure)); + MOCK_METHOD(void, fetch, + (Http::RequestMessage & message, Tracing::Span& parent_span, + MetadataFetcher::MetadataReceiver& receiver)); +}; + +class MockMetadataReceiver : public MetadataFetcher::MetadataReceiver { +public: + MOCK_METHOD(void, onMetadataSuccess, (const std::string&& body)); + MOCK_METHOD(void, onMetadataError, (MetadataFetcher::MetadataReceiver::Failure reason)); +}; + class MockCredentialsProvider : public CredentialsProvider { public: MockCredentialsProvider(); diff --git a/test/extensions/common/aws/utility_test.cc b/test/extensions/common/aws/utility_test.cc index 629f28c2c9577..221a94454d130 100644 --- a/test/extensions/common/aws/utility_test.cc +++ b/test/extensions/common/aws/utility_test.cc @@ -1,11 +1,18 @@ #include "source/extensions/common/aws/utility.h" +#include "test/extensions/common/aws/mocks.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" +using testing::_; using testing::ElementsAre; +using testing::InSequence; +using testing::NiceMock; using testing::Pair; +using testing::Ref; +using testing::Return; +using testing::Throw; namespace Envoy { namespace Extensions { @@ -13,6 +20,12 @@ namespace Common { namespace Aws { namespace { +MATCHER_P(WithName, expectedName, "") { + *result_listener << "\nexpected { name: \"" << expectedName << "\"} but got {name: \"" + << arg.name() << "\"}\n"; + return ExplainMatchResult(expectedName, arg.name(), result_listener); +} + // Headers must be in alphabetical order by virtue of std::map TEST(UtilityTest, CanonicalizeHeadersInAlphabeticalOrder) { Http::TestRequestHeaderMapImpl headers{ @@ -346,6 +359,45 @@ TEST(UtilityTest, JoinCanonicalHeaderNamesWithEmptyMap) { EXPECT_EQ("", names); } +// Verify that we don't add a thread local cluster if it already exists. +TEST(UtilityTest, ThreadLocalClusterExistsAlready) { + NiceMock cluster_; + NiceMock cm_; + EXPECT_CALL(cm_, getThreadLocalCluster(_)).WillOnce(Return(&cluster_)); + EXPECT_CALL(cm_, addOrUpdateCluster(_, _)).Times(0); + EXPECT_TRUE(Utility::addInternalClusterStatic(cm_, "cluster_name", + envoy::config::cluster::v3::Cluster::STATIC, "")); +} + +// Verify that if thread local cluster doesn't exist we can create a new one. +TEST(UtilityTest, AddStaticClusterSuccess) { + NiceMock cm_; + EXPECT_CALL(cm_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cm_, addOrUpdateCluster(WithName("cluster_name"), _)).WillOnce(Return(true)); + EXPECT_TRUE(Utility::addInternalClusterStatic( + cm_, "cluster_name", envoy::config::cluster::v3::Cluster::STATIC, "127.0.0.1:80")); +} + +// Handle exception when adding thread local cluster fails. +TEST(UtilityTest, AddStaticClusterFailure) { + NiceMock cm_; + EXPECT_CALL(cm_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cm_, addOrUpdateCluster(WithName("cluster_name"), _)) + .WillOnce(Throw(EnvoyException("exeption message"))); + EXPECT_FALSE(Utility::addInternalClusterStatic( + cm_, "cluster_name", envoy::config::cluster::v3::Cluster::STATIC, "127.0.0.1:80")); +} + +// Verify that for uri argument in addInternalClusterStatic port value is optional +// and can contain request path which will be ignored. +TEST(UtilityTest, AddStaticClusterSuccessEvenWithMissingPort) { + NiceMock cm_; + EXPECT_CALL(cm_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cm_, addOrUpdateCluster(WithName("cluster_name"), _)).WillOnce(Return(true)); + EXPECT_TRUE(Utility::addInternalClusterStatic( + cm_, "cluster_name", envoy::config::cluster::v3::Cluster::STATIC, "127.0.0.1/something")); +} + } // namespace } // namespace Aws } // namespace Common