diff --git a/source/common/config/datasource.h b/source/common/config/datasource.h index 1e35e119518b..25aa4a90a28d 100644 --- a/source/common/config/datasource.h +++ b/source/common/config/datasource.h @@ -2,6 +2,7 @@ #include "envoy/api/api.h" #include "envoy/config/core/v3/base.pb.h" +#include "envoy/event/deferred_deletable.h" #include "envoy/init/manager.h" #include "envoy/upstream/cluster_manager.h" @@ -58,7 +59,8 @@ class LocalAsyncDataProvider { using LocalAsyncDataProviderPtr = std::unique_ptr; -class RemoteAsyncDataProvider : public Config::DataFetcher::RemoteDataFetcherCallback, +class RemoteAsyncDataProvider : public Event::DeferredDeletable, + public Config::DataFetcher::RemoteDataFetcherCallback, public Logger::Loggable { public: RemoteAsyncDataProvider(Upstream::ClusterManager& cm, Init::Manager& manager, diff --git a/source/extensions/common/wasm/wasm.cc b/source/extensions/common/wasm/wasm.cc index 7131ec00ff69..ece070e4193d 100644 --- a/source/extensions/common/wasm/wasm.cc +++ b/source/extensions/common/wasm/wasm.cc @@ -1,35 +1,12 @@ #include "extensions/common/wasm/wasm.h" -#include -#include -#include -#include -#include - -#include "envoy/common/exception.h" -#include "envoy/config/wasm/v3/wasm.pb.validate.h" -#include "envoy/grpc/status.h" -#include "envoy/http/codes.h" -#include "envoy/local_info/local_info.h" -#include "envoy/thread_local/thread_local.h" - -#include "common/common/assert.h" -#include "common/common/base64.h" -#include "common/common/empty_string.h" -#include "common/common/enum_to_int.h" -#include "common/common/logger.h" +#include + +#include "envoy/event/deferred_deletable.h" -#include "extensions/common/wasm/wasm_vm.h" -#include "extensions/common/wasm/well_known_names.h" +#include "common/common/logger.h" -#include "absl/base/casts.h" -#include "absl/container/flat_hash_map.h" -#include "absl/container/node_hash_map.h" #include "absl/strings/str_cat.h" -#include "absl/synchronization/mutex.h" -#include "openssl/bytestring.h" -#include "openssl/hmac.h" -#include "openssl/sha.h" namespace Envoy { namespace Extensions { @@ -38,9 +15,22 @@ namespace Wasm { namespace { +struct CodeCacheEntry { + std::string code; + bool in_progress; + MonotonicTime fetch_time; +}; + const std::string INLINE_STRING = ""; +// NB: xDS currently does not support failing asynchronously, so we fail immediately +// if remote Wasm code is not cached and do a background fill. +const bool DEFAULT_FAIL_IF_NOT_CACHED = true; +bool fail_if_code_not_cached = DEFAULT_FAIL_IF_NOT_CACHED; +const int CODE_CACHE_SECONDS_NEGATIVE_CACHING = 10; -std::atomic active_wasm_; +std::atomic active_wasms; +std::mutex code_cache_mutex; +std::unordered_map* code_cache = nullptr; // Downcast WasmBase to the actual Wasm. inline Wasm* getWasm(WasmHandleSharedPtr& base_wasm_handle) { @@ -50,8 +40,8 @@ inline Wasm* getWasm(WasmHandleSharedPtr& base_wasm_handle) { } // namespace void Wasm::initializeStats() { - active_wasm_++; - wasm_stats_.active_.set(active_wasm_); + active_wasms++; + wasm_stats_.active_.set(active_wasms); wasm_stats_.created_.inc(); } @@ -76,7 +66,7 @@ Wasm::Wasm(absl::string_view runtime, absl::string_view vm_id, absl::string_view ALL_WASM_STATS(POOL_COUNTER_PREFIX(*scope_, absl::StrCat("wasm.", runtime, ".")), POOL_GAUGE_PREFIX(*scope_, absl::StrCat("wasm.", runtime, ".")))}) { initializeStats(); - ENVOY_LOG(debug, "Base Wasm created {} now active", active_wasm_); + ENVOY_LOG(debug, "Base Wasm created {} now active", active_wasms); } Wasm::Wasm(WasmHandleSharedPtr base_wasm_handle, Event::Dispatcher& dispatcher) @@ -90,7 +80,7 @@ Wasm::Wasm(WasmHandleSharedPtr base_wasm_handle, Event::Dispatcher& dispatcher) cluster_manager_(getWasm(base_wasm_handle)->clusterManager()), dispatcher_(dispatcher), time_source_(dispatcher.timeSource()), wasm_stats_(getWasm(base_wasm_handle)->wasm_stats_) { initializeStats(); - ENVOY_LOG(debug, "Thread-Local Wasm created {} now active", active_wasm_); + ENVOY_LOG(debug, "Thread-Local Wasm created {} now active", active_wasms); } void Wasm::setTickPeriod(uint32_t context_id, std::chrono::milliseconds new_tick_period) { @@ -127,9 +117,9 @@ void Wasm::tickHandler(uint32_t root_context_id) { } Wasm::~Wasm() { - active_wasm_--; - wasm_stats_.active_.set(active_wasm_); - ENVOY_LOG(debug, "~Wasm {} remaining active", active_wasm_); + active_wasms--; + wasm_stats_.active_.set(active_wasms); + ENVOY_LOG(debug, "~Wasm {} remaining active", active_wasms); if (server_shutdown_post_cb_) { dispatcher_.post(server_shutdown_post_cb_); } @@ -155,8 +145,17 @@ void Wasm::log(absl::string_view root_id, const Http::RequestHeaderMap* request_ context->log(request_headers, response_headers, response_trailers, stream_info); } +void clearCodeCacheForTesting(bool fail_if_not_cached) { + std::lock_guard guard(code_cache_mutex); + fail_if_code_not_cached = fail_if_not_cached; + if (code_cache) { + delete code_cache; + code_cache = nullptr; + } +} + static void createWasmInternal(const VmConfig& vm_config, const PluginSharedPtr& plugin, - Stats::ScopeSharedPtr scope, + const Stats::ScopeSharedPtr& scope, Upstream::ClusterManager& cluster_manager, Init::Manager& init_manager, Event::Dispatcher& dispatcher, Runtime::RandomGenerator& random, Api::Api& api, @@ -165,45 +164,98 @@ static void createWasmInternal(const VmConfig& vm_config, const PluginSharedPtr& std::unique_ptr root_context_for_testing, CreateWasmCallback&& cb) { std::string source, code; + bool fetch = false; if (vm_config.code().has_remote()) { source = vm_config.code().remote().http_uri().uri(); + std::lock_guard guard(code_cache_mutex); + if (!code_cache) { + code_cache = new std::remove_reference::type; + } + auto it = code_cache->find(vm_config.code().remote().sha256()); + if (it != code_cache->end()) { + if (it->second.in_progress) { + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), warn, + "createWasm: failed to load (in prpgress) from {}", source); + throw WasmException( + fmt::format("Failed to load WASM code (fetch in progress) from {}", source)); + } + code = it->second.code; + if (code.empty()) { + if (dispatcher.timeSource().monotonicTime() - it->second.fetch_time < + std::chrono::seconds(CODE_CACHE_SECONDS_NEGATIVE_CACHING)) { + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), warn, + "createWasm: failed to load (cached) from {}", source); + throw WasmException(fmt::format("Failed to load WASM code (cached) from {}", source)); + } + fetch = true; // Fetch failed, retry. + it->second.in_progress = true; + it->second.fetch_time = dispatcher.timeSource().monotonicTime(); + } + } else { + fetch = true; // Not in cache, fetch. + auto& e = (*code_cache)[vm_config.code().remote().sha256()]; + e.in_progress = true; + e.fetch_time = dispatcher.timeSource().monotonicTime(); + } } else if (vm_config.code().has_local()) { code = Config::DataSource::read(vm_config.code().local(), true, api); source = Config::DataSource::getPath(vm_config.code().local()) .value_or(code.empty() ? EMPTY_STRING : INLINE_STRING); } - auto callback = [vm_config, scope, &cluster_manager, &dispatcher, &lifecycle_notifier, plugin, cb, - source, - context_ptr = root_context_for_testing ? root_context_for_testing.release() - : nullptr](const std::string& code) { - if (code.empty()) { - throw WasmException(fmt::format("Failed to load WASM code from {}", source)); - } - std::unique_ptr context(context_ptr); - auto configuration = vm_config.configuration(); - auto vm_key = proxy_wasm::makeVmKey(vm_config.vm_id(), configuration, code); - cb(std::static_pointer_cast(proxy_wasm::createWasm( - vm_key, code, plugin, - [&vm_config, &configuration, &scope, &cluster_manager, &dispatcher, - &lifecycle_notifier](absl::string_view vm_key) { - auto wasm = std::make_shared(vm_config.runtime(), vm_config.vm_id(), configuration, - vm_key, scope, cluster_manager, dispatcher); - // NB: we need the shared_ptr to have been created for shared_from_this() to work. - wasm->initializeLifecycle(lifecycle_notifier); - return std::static_pointer_cast(std::make_shared(wasm)); - }, - vm_config.allow_precompiled(), std::move(context)))); - }; + auto complete_cb = + [cb, vm_config, plugin, scope, &cluster_manager, &dispatcher, &lifecycle_notifier, + root_context_for_testing_ptr = root_context_for_testing.release()](std::string code) { + auto root_context = std::unique_ptr(root_context_for_testing_ptr); + auto vm_key = proxy_wasm::makeVmKey(vm_config.vm_id(), vm_config.configuration(), code); + cb(std::static_pointer_cast(proxy_wasm::createWasm( + vm_key, code, plugin, + [&vm_config, &scope, &cluster_manager, &dispatcher, + &lifecycle_notifier](absl::string_view vm_key) { + auto wasm = std::make_shared(vm_config.runtime(), vm_config.vm_id(), + vm_config.configuration(), vm_key, scope, + cluster_manager, dispatcher); + // NB: we need the shared_ptr to have been created for shared_from_this() to work. + wasm->initializeLifecycle(lifecycle_notifier); + return std::static_pointer_cast(std::make_shared(wasm)); + }, + vm_config.allow_precompiled(), std::move(root_context)))); + }; - if (vm_config.code().has_remote()) { + if (fetch) { + // NB: if the (fetching) exception is thrown below, the remote_data provider will be deleted + // immediately rather than completing the async fetch, so allow for self-delete. + auto remote_data_provider_holder = + std::make_shared>(); + auto fetch_callback = [vm_config, complete_cb, source, &dispatcher, + remote_data_provider_holder](const std::string& code) { + { + std::lock_guard guard(code_cache_mutex); + auto& e = (*code_cache)[vm_config.code().remote().sha256()]; + e.in_progress = false; + e.code = code; + } + if (!fail_if_code_not_cached) { + if (code.empty()) { + throw WasmException( + fmt::format("Failed to load WASM code (fetch failed) from {}", source)); + } + complete_cb(code); + } + // NB: must be deleted explicitly. + dispatcher.deferredDelete( + Envoy::Event::DeferredDeletablePtr{remote_data_provider_holder->release()}); + remote_data_provider_holder->reset(); + }; remote_data_provider = std::make_unique( cluster_manager, init_manager, vm_config.code().remote(), dispatcher, random, true, - std::move(callback)); - } else if (vm_config.code().has_local()) { - callback(code); + fetch_callback); + if (fail_if_code_not_cached) { + *remote_data_provider_holder = std::move(remote_data_provider); + throw WasmException(fmt::format("Failed to load WASM code (fetching) from {}", source)); + } } else { - callback(EMPTY_STRING); + complete_cb(code); } } diff --git a/source/extensions/common/wasm/wasm.h b/source/extensions/common/wasm/wasm.h index a6e96802ad64..fbe2f0c77c38 100644 --- a/source/extensions/common/wasm/wasm.h +++ b/source/extensions/common/wasm/wasm.h @@ -132,6 +132,8 @@ WasmHandleSharedPtr getOrCreateThreadLocalWasm(const WasmHandleSharedPtr& base_w const PluginSharedPtr& plugin, Event::Dispatcher& dispatcher); +void clearCodeCacheForTesting(bool fail_if_not_cached); + } // namespace Wasm } // namespace Common } // namespace Extensions diff --git a/test/extensions/access_loggers/wasm/config_test.cc b/test/extensions/access_loggers/wasm/config_test.cc index 5212b91a5592..9e83b867f7ef 100644 --- a/test/extensions/access_loggers/wasm/config_test.cc +++ b/test/extensions/access_loggers/wasm/config_test.cc @@ -62,7 +62,7 @@ TEST_P(WasmAccessLogConfigTest, CreateWasmFromEmpty) { AccessLog::InstanceSharedPtr instance; EXPECT_THROW_WITH_MESSAGE( instance = factory->createAccessLogInstance(*message, std::move(filter), context), - Common::Wasm::WasmException, "Failed to load WASM code from "); + Common::Wasm::WasmException, "Failed to create WASM VM with unspecified runtime."); } TEST_P(WasmAccessLogConfigTest, CreateWasmFromWASM) { diff --git a/test/extensions/common/wasm/wasm_test.cc b/test/extensions/common/wasm/wasm_test.cc index b31bb7180df7..332164e2216f 100644 --- a/test/extensions/common/wasm/wasm_test.cc +++ b/test/extensions/common/wasm/wasm_test.cc @@ -58,7 +58,10 @@ class TestContext : public Extensions::Common::Wasm::Context { MOCK_METHOD2(log_, void(spdlog::level::level_enum level, absl::string_view message)); }; -class WasmCommonTest : public testing::TestWithParam {}; +class WasmCommonTest : public testing::TestWithParam { +public: + void SetUp() { clearCodeCacheForTesting(false); } +}; INSTANTIATE_TEST_SUITE_P(Runtimes, WasmCommonTest, testing::Values("v8", diff --git a/test/extensions/filters/http/wasm/config_test.cc b/test/extensions/filters/http/wasm/config_test.cc index 464ea4c9e9c0..3b883224adc6 100644 --- a/test/extensions/filters/http/wasm/config_test.cc +++ b/test/extensions/filters/http/wasm/config_test.cc @@ -1,3 +1,5 @@ +#include + #include "envoy/extensions/filters/http/wasm/v3/wasm.pb.validate.h" #include "common/common/base64.h" @@ -29,11 +31,13 @@ class WasmFilterConfigTest : public testing::TestWithParam { ON_CALL(context_, api()).WillByDefault(ReturnRef(*api_)); ON_CALL(context_, scope()).WillByDefault(ReturnRef(stats_store_)); ON_CALL(context_, listenerMetadata()).WillByDefault(ReturnRef(listener_metadata_)); - ON_CALL(context_, initManager()).WillByDefault(ReturnRef(init_manager_)); + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager_)); ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); ON_CALL(context_, dispatcher()).WillByDefault(ReturnRef(dispatcher_)); } + void SetUp() { Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(false); } + void initializeForRemote() { retry_timer_ = new Event::MockTimer(); @@ -207,6 +211,176 @@ TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteWASM) { cb(filter_callback); } +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteWASMFailOnUncachedThenSucceed) { + Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(true); + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/headers_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body() = std::make_unique(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (fetching) from https://example.com/data"); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + + cb(filter_callback); + dispatcher_.clearDeferredDeleteList(); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteWASMFailCachedThenSucceed) { + Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(true); + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/headers_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillRepeatedly(ReturnRef(cluster_manager_.async_client_)); + + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callbacks.onSuccess( + request, + Http::ResponseMessagePtr{new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "503"}}})}); + return &request; + })); + + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (fetching) from https://example.com/data"); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (cached) from https://example.com/data"); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Init::ManagerImpl init_manager3{"init_manager3"}; + Init::ExpectableWatcherImpl init_watcher3; + + dispatcher_.time_system_.advanceTimeWait(std::chrono::seconds(30)); + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager3)); + + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body() = std::make_unique(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (fetching) from https://example.com/data"); + EXPECT_CALL(init_watcher3, ready()); + init_manager3.initialize(init_watcher3); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Init::ManagerImpl init_manager4{"init_manager4"}; + Init::ExpectableWatcherImpl init_watcher4; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager4)); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + + EXPECT_CALL(init_watcher4, ready()); + init_manager4.initialize(init_watcher4); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + + cb(filter_callback); + dispatcher_.clearDeferredDeleteList(); +} + TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteConnectionReset) { const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/headers_cpp.wasm")); @@ -245,9 +419,9 @@ TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteConnectionReset) { Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to load WASM code from https://example.com/data"); + EXPECT_THROW_WITH_MESSAGE( + context_.initManager().initialize(init_watcher_), Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (fetch failed) from https://example.com/data"); } TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessWith503) { @@ -291,9 +465,9 @@ TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessWith503) { Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to load WASM code from https://example.com/data"); + EXPECT_THROW_WITH_MESSAGE( + context_.initManager().initialize(init_watcher_), Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (fetch failed) from https://example.com/data"); } TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessIncorrectSha256) { @@ -337,9 +511,9 @@ TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessIncorrectSha256) { Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to load WASM code from https://example.com/data"); + EXPECT_THROW_WITH_MESSAGE( + context_.initManager().initialize(init_watcher_), Extensions::Common::Wasm::WasmException, + "Failed to load WASM code (fetch failed) from https://example.com/data"); } TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteMultipleRetries) { diff --git a/test/extensions/filters/http/wasm/wasm_filter_test.cc b/test/extensions/filters/http/wasm/wasm_filter_test.cc index 8e9b97215577..29c77d96c9c4 100644 --- a/test/extensions/filters/http/wasm/wasm_filter_test.cc +++ b/test/extensions/filters/http/wasm/wasm_filter_test.cc @@ -95,6 +95,8 @@ class WasmHttpFilterTest : public testing::TestWithParam { WasmHttpFilterTest() {} ~WasmHttpFilterTest() {} + void SetUp() { Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(false); } + void setupConfig(const std::string& code, std::string root_id = "") { root_context_ = new TestRoot(); WasmFilterConfig proto_config; diff --git a/test/extensions/filters/network/wasm/config_test.cc b/test/extensions/filters/network/wasm/config_test.cc index 6b4bae395a04..509d5f4d7632 100644 --- a/test/extensions/filters/network/wasm/config_test.cc +++ b/test/extensions/filters/network/wasm/config_test.cc @@ -32,6 +32,8 @@ class WasmNetworkFilterConfigTest : public testing::TestWithParam { ON_CALL(context_, dispatcher()).WillByDefault(ReturnRef(dispatcher_)); } + void SetUp() override { Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(false); } + void initializeForRemote() { retry_timer_ = new Event::MockTimer(); @@ -128,304 +130,6 @@ TEST_P(WasmNetworkFilterConfigTest, YamlLoadInlineBadCode) { "Failed to initialize WASM code"); } -TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromRemoteWASM) { - const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/logging_cpp.wasm")); - const std::string sha256 = Hex::encode( - Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); - const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( - config: - vm_config: - runtime: "envoy.wasm.runtime.)EOF", - GetParam(), R"EOF(" - code: - remote: - http_uri: - uri: https://example.com/data - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256)); - envoy::extensions::filters::network::wasm::v3::Wasm proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - WasmFilterConfig factory; - NiceMock client; - NiceMock request(&client); - - EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) - .WillOnce(ReturnRef(cluster_manager_.async_client_)); - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .WillOnce( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - Http::ResponseMessagePtr response( - new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body() = std::make_unique(code); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); - Network::MockConnection connection; - EXPECT_CALL(connection, addFilter(_)); - cb(connection); -} - -TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromRemoteConnectionReset) { - const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/logging_cpp.wasm")); - const std::string sha256 = Hex::encode( - Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); - const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( - config: - vm_config: - runtime: "envoy.wasm.runtime.)EOF", - GetParam(), R"EOF(" - code: - remote: - http_uri: - uri: https://example.com/data - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 0 - sha256: )EOF", - sha256)); - envoy::extensions::filters::network::wasm::v3::Wasm proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - WasmFilterConfig factory; - NiceMock client; - NiceMock request(&client); - - EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) - .WillOnce(ReturnRef(cluster_manager_.async_client_)); - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .WillOnce( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - callbacks.onFailure(request, Envoy::Http::AsyncClient::FailureReason::Reset); - return &request; - })); - - Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); - EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to load WASM code from https://example.com/data"); -} - -TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromRemoteSuccessWith503) { - const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/logging_cpp.wasm")); - const std::string sha256 = Hex::encode( - Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); - const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( - config: - vm_config: - runtime: "envoy.wasm.runtime.)EOF", - GetParam(), R"EOF(" - code: - remote: - http_uri: - uri: https://example.com/data - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 0 - sha256: )EOF", - sha256)); - envoy::extensions::filters::network::wasm::v3::Wasm proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - WasmFilterConfig factory; - NiceMock client; - NiceMock request(&client); - - EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) - .WillOnce(ReturnRef(cluster_manager_.async_client_)); - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .WillOnce( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - callbacks.onSuccess( - request, - Http::ResponseMessagePtr{new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "503"}}})}); - return &request; - })); - - Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); - EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to load WASM code from https://example.com/data"); -} - -TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromRemoteSuccessIncorrectSha256) { - const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/logging_cpp.wasm")); - const std::string sha256 = Hex::encode( - Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); - const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( - config: - vm_config: - runtime: "envoy.wasm.runtime.)EOF", - GetParam(), R"EOF(" - code: - remote: - http_uri: - uri: https://example.com/data - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 0 - sha256: xxxx )EOF")); - envoy::extensions::filters::network::wasm::v3::Wasm proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - WasmFilterConfig factory; - NiceMock client; - NiceMock request(&client); - - EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) - .WillOnce(ReturnRef(cluster_manager_.async_client_)); - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .WillOnce( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - Http::ResponseMessagePtr response( - new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body() = std::make_unique(code); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); - EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to load WASM code from https://example.com/data"); -} - -TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromRemoteMultipleRetries) { - initializeForRemote(); - const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/logging_cpp.wasm")); - const std::string sha256 = Hex::encode( - Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); - const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( - config: - vm_config: - runtime: "envoy.wasm.runtime.)EOF", - GetParam(), R"EOF(" - code: - remote: - http_uri: - uri: https://example.com/data - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 3 - sha256: )EOF", - sha256)); - envoy::extensions::filters::network::wasm::v3::Wasm proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - WasmFilterConfig factory; - NiceMock client; - NiceMock request(&client); - int num_retries = 3; - EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) - .WillRepeatedly(ReturnRef(cluster_manager_.async_client_)); - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .Times(num_retries) - .WillRepeatedly( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - Http::ResponseMessagePtr response( - new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "503"}}})); - response->body() = std::make_unique(code); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - EXPECT_CALL(*retry_timer_, enableTimer(_, _)) - .WillRepeatedly(Invoke([&](const std::chrono::milliseconds&, const ScopeTrackedObject*) { - if (--num_retries == 0) { - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .WillOnce(Invoke( - [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - Http::ResponseMessagePtr response( - new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body() = std::make_unique(code); - - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - } - - retry_timer_cb_(); - })); - EXPECT_CALL(*retry_timer_, disableTimer()); - - Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); - Network::MockConnection connection; - EXPECT_CALL(connection, addFilter(_)); - cb(connection); -} - -TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromRemoteSuccessBadcode) { - const std::string code = "foo"; - const std::string sha256 = Hex::encode( - Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); - const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( - config: - vm_config: - runtime: "envoy.wasm.runtime.)EOF", - GetParam(), R"EOF(" - code: - remote: - http_uri: - uri: https://example.com/data - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256)); - envoy::extensions::filters::network::wasm::v3::Wasm proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - WasmFilterConfig factory; - NiceMock client; - NiceMock request(&client); - - EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) - .WillOnce(ReturnRef(cluster_manager_.async_client_)); - EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) - .WillOnce( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - Http::ResponseMessagePtr response( - new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body() = std::make_unique(code); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); - EXPECT_CALL(init_watcher_, ready()); - EXPECT_THROW_WITH_MESSAGE(context_.initManager().initialize(init_watcher_), - Extensions::Common::Wasm::WasmException, - "Failed to initialize WASM code"); -} - } // namespace Wasm } // namespace NetworkFilters } // namespace Extensions