From 74ae885dfb49bdc7a55b3b2a0886b65d85dd261b Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Feb 2026 10:13:12 +0530 Subject: [PATCH 01/21] dynamic modules: add remote fetching for the module binary Signed-off-by: Anurag Aggarwal --- api/envoy/extensions/dynamic_modules/v3/BUILD | 5 +- .../dynamic_modules/v3/dynamic_modules.proto | 52 ++- source/extensions/dynamic_modules/BUILD | 22 ++ .../extensions/dynamic_modules/code_cache.cc | 151 +++++++++ .../extensions/dynamic_modules/code_cache.h | 158 +++++++++ .../dynamic_modules/dynamic_modules.cc | 80 +++++ .../dynamic_modules/dynamic_modules.h | 22 ++ .../filters/http/dynamic_modules/BUILD | 8 + .../filters/http/dynamic_modules/factory.cc | 302 +++++++++++++++++- .../filters/http/dynamic_modules/factory.h | 16 +- test/extensions/dynamic_modules/BUILD | 13 + .../dynamic_modules/code_cache_test.cc | 179 +++++++++++ .../dynamic_modules/dynamic_modules_test.cc | 97 ++++++ .../dynamic_modules/test_data/c/BUILD | 1 + .../filters/http/dynamic_modules/BUILD | 35 ++ .../http/dynamic_modules/config_test.cc | 278 ++++++++++++++++ 16 files changed, 1400 insertions(+), 19 deletions(-) create mode 100644 source/extensions/dynamic_modules/code_cache.cc create mode 100644 source/extensions/dynamic_modules/code_cache.h create mode 100644 test/extensions/dynamic_modules/code_cache_test.cc create mode 100644 test/extensions/filters/http/dynamic_modules/BUILD create mode 100644 test/extensions/filters/http/dynamic_modules/config_test.cc diff --git a/api/envoy/extensions/dynamic_modules/v3/BUILD b/api/envoy/extensions/dynamic_modules/v3/BUILD index 29ebf0741406e..09a37ad16b837 100644 --- a/api/envoy/extensions/dynamic_modules/v3/BUILD +++ b/api/envoy/extensions/dynamic_modules/v3/BUILD @@ -5,5 +5,8 @@ load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") licenses(["notice"]) # Apache 2 api_proto_package( - deps = ["@com_github_cncf_xds//udpa/annotations:pkg"], + deps = [ + "//envoy/config/core/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], ) diff --git a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto index 139d16aa5ae25..957290af333e0 100644 --- a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto +++ b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.extensions.dynamic_modules.v3; +import "envoy/config/core/v3/base.proto"; + import "udpa/annotations/status.proto"; import "validate/validate.proto"; @@ -30,7 +32,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // the ABI is stabilized, this restriction will be revisited. Until then, Envoy checks the hash of // the ABI header files to ensure that the dynamic modules are built against the same version of the // ABI. -// [#next-free-field: 6] +// [#next-free-field: 8] message DynamicModuleConfig { // The name of the dynamic module. // @@ -42,9 +44,12 @@ message DynamicModuleConfig { // try to find the module from a standard system library path (e.g., ``/usr/lib``) following the // platform's default behavior for ``dlopen``. // + // This field is optional if the ``module`` field is set. When both ``name`` and ``module`` are + // specified, the ``module`` field takes precedence. + // // .. note:: // There is some remaining work to make the search path configurable via command line options. - string name = 1 [(validate.rules).string = {min_len: 1}]; + string name = 1; // If true, prevents the module from being unloaded with ``dlclose``. // @@ -78,4 +83,47 @@ message DynamicModuleConfig { // // Defaults to ``dynamicmodulescustom``. string metrics_namespace = 5; + + // The dynamic module binary to load. + // + // This field supports loading modules from: + // + // * Local file path (via ``local.filename``) + // * Inline bytes (via ``local.inline_bytes`` or ``local.inline_string``) + // * Remote HTTP URL (via ``remote``) + // + // When both ``name`` and ``module`` are set, ``module`` takes precedence. + // + // Example configurations: + // + // Local file: + // + // .. code-block:: yaml + // + // module: + // local: + // filename: "/path/to/libmy_module.so" + // + // Remote HTTP: + // + // .. code-block:: yaml + // + // module: + // remote: + // http_uri: + // uri: "https://modules.example.com/libmy_module.so" + // cluster: module_server + // timeout: 10s + // sha256: "abc123..." + // + config.core.v3.AsyncDataSource module = 6; + + // If true and the module needs to be remotely fetched and it is not in the cache, + // then NACK the configuration update and do a background fetch to fill the cache. + // If false, fetch the module asynchronously and enter warming state. + // + // This only applies when using remote data sources via the ``module`` field. + // + // Defaults to ``false``. + bool nack_on_module_cache_miss = 7; } diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index e3a294ae09dbc..c39730296c9a4 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -17,10 +17,32 @@ envoy_cc_library( ], deps = [ "//envoy/common:exception_lib", + "//source/common/buffer:buffer_lib", + "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", "//source/extensions/dynamic_modules/abi", ], ) +envoy_cc_library( + name = "code_cache_lib", + srcs = ["code_cache.cc"], + hdrs = [ + "code_cache.h", + ], + deps = [ + "//envoy/event:deferred_deletable", + "//envoy/event:dispatcher_interface", + "//envoy/init:manager_interface", + "//envoy/stats:stats_interface", + "//envoy/upstream:cluster_manager_interface", + "//source/common/common:logger_lib", + "//source/common/config:remote_data_fetcher_lib", + "//source/common/init:target_lib", + "@com_google_absl//absl/container:flat_hash_map", + ], +) + envoy_cc_library( name = "abi_impl", srcs = ["abi_impl.cc"], diff --git a/source/extensions/dynamic_modules/code_cache.cc b/source/extensions/dynamic_modules/code_cache.cc new file mode 100644 index 0000000000000..1c9bc68dfa756 --- /dev/null +++ b/source/extensions/dynamic_modules/code_cache.cc @@ -0,0 +1,151 @@ +#include "source/extensions/dynamic_modules/code_cache.h" + +#include "source/common/common/lock_guard.h" +#include "source/common/common/thread.h" + +namespace Envoy { +namespace Extensions { +namespace DynamicModules { + +namespace { +// Global code cache instance. +std::unique_ptr global_code_cache; +Thread::MutexBasicLockable global_code_cache_mutex; +} // namespace + +DynamicModuleCodeCache& getCodeCache() { + Thread::LockGuard guard(global_code_cache_mutex); + if (!global_code_cache) { + global_code_cache = std::make_unique(); + } + return *global_code_cache; +} + +void clearCodeCacheForTesting() { + Thread::LockGuard guard(global_code_cache_mutex); + if (global_code_cache) { + global_code_cache->clear(); + } +} + +void setTimeOffsetForCodeCacheForTesting(MonotonicTime::duration d) { + Thread::LockGuard guard(global_code_cache_mutex); + if (global_code_cache) { + global_code_cache->setTimeOffsetForTesting(d); + } +} + +CacheLookupResult DynamicModuleCodeCache::lookup(const std::string& key, MonotonicTime now) { + Thread::LockGuard guard(mutex_); + + // Apply time offset for testing. + now += time_offset_for_testing_; + + // Remove expired entries. + removeExpiredEntries(now); + + auto it = cache_.find(key); + if (it == cache_.end()) { + return CacheLookupResult{"", false, false}; + } + + CodeCacheEntry& entry = it->second; + entry.use_time = now; + + if (entry.in_progress) { + return CacheLookupResult{"", true, true}; + } + + // Check if this is a negative cache entry (empty code with recent fetch). + if (entry.code.empty()) { + auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); + if (elapsed < NEGATIVE_CACHE_SECONDS) { + // Still within negative cache TTL - return empty but mark as cache hit. + return CacheLookupResult{"", false, true}; + } + // Negative cache expired - treat as cache miss. + cache_.erase(it); + return CacheLookupResult{"", false, false}; + } + + // Check TTL for positive cache entries. + auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); + if (elapsed >= CACHE_TTL_SECONDS) { + cache_.erase(it); + return CacheLookupResult{"", false, false}; + } + + return CacheLookupResult{entry.code, false, true}; +} + +void DynamicModuleCodeCache::markInProgress(const std::string& key, MonotonicTime now) { + Thread::LockGuard guard(mutex_); + now += time_offset_for_testing_; + + CodeCacheEntry& entry = cache_[key]; + entry.in_progress = true; + entry.use_time = now; + entry.fetch_time = now; +} + +void DynamicModuleCodeCache::update(const std::string& key, const std::string& code, + MonotonicTime now) { + Thread::LockGuard guard(mutex_); + now += time_offset_for_testing_; + + CodeCacheEntry& entry = cache_[key]; + entry.code = code; + entry.in_progress = false; + entry.use_time = now; + entry.fetch_time = now; +} + +size_t DynamicModuleCodeCache::size() const { + Thread::LockGuard guard(mutex_); + return cache_.size(); +} + +void DynamicModuleCodeCache::clear() { + Thread::LockGuard guard(mutex_); + cache_.clear(); + time_offset_for_testing_ = MonotonicTime::duration{}; +} + +void DynamicModuleCodeCache::setTimeOffsetForTesting(MonotonicTime::duration offset) { + Thread::LockGuard guard(mutex_); + time_offset_for_testing_ = offset; +} + +void DynamicModuleCodeCache::removeExpiredEntries(MonotonicTime now) { + // Called with mutex held. + for (auto it = cache_.begin(); it != cache_.end();) { + const CodeCacheEntry& entry = it->second; + + // Don't remove in-progress entries. + if (entry.in_progress) { + ++it; + continue; + } + + auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); + + bool expired = false; + if (entry.code.empty()) { + // Negative cache entry. + expired = elapsed >= NEGATIVE_CACHE_SECONDS; + } else { + // Positive cache entry. + expired = elapsed >= CACHE_TTL_SECONDS; + } + + if (expired) { + cache_.erase(it++); + } else { + ++it; + } + } +} + +} // namespace DynamicModules +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/dynamic_modules/code_cache.h b/source/extensions/dynamic_modules/code_cache.h new file mode 100644 index 0000000000000..21f7ac80a039c --- /dev/null +++ b/source/extensions/dynamic_modules/code_cache.h @@ -0,0 +1,158 @@ +#pragma once + +#include +#include +#include + +#include "envoy/event/deferred_deletable.h" +#include "envoy/event/dispatcher.h" +#include "envoy/init/manager.h" +#include "envoy/stats/scope.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/common/lock_guard.h" +#include "source/common/common/logger.h" +#include "source/common/common/thread.h" +#include "source/common/config/remote_data_fetcher.h" +#include "source/common/init/target_impl.h" + +#include "absl/container/flat_hash_map.h" + +namespace Envoy { +namespace Extensions { +namespace DynamicModules { + +/** + * Represents an entry in the code cache. + */ +struct CodeCacheEntry { + std::string code; // Module binary data. + bool in_progress; // Fetch is ongoing. + MonotonicTime use_time; // Last access time. + MonotonicTime fetch_time; // When the module was fetched. +}; + +/** + * Result of a cache lookup operation. + */ +struct CacheLookupResult { + // The cached code if found and valid, empty string otherwise. + std::string code; + // True if a fetch operation is already in progress for this key. + bool fetch_in_progress; + // True if a cache entry exists (even if in_progress or expired for negative cache). + bool cache_hit; +}; + +/** + * Callback invoked when async fetch completes. + * @param code The fetched module bytes, or empty string on failure. + */ +using FetchCallback = std::function; + +/** + * Thread-safe code cache for dynamic modules. + * + * Features: + * - Keyed by SHA256 hash of module content. + * - 24-hour TTL for cached entries. + * - 10-second negative caching for failed fetches. + * - In-progress tracking to avoid duplicate fetches. + */ +class DynamicModuleCodeCache : public Logger::Loggable { +public: + // Cache TTL in seconds (24 hours). + static constexpr int CACHE_TTL_SECONDS = 24 * 3600; + // Negative cache TTL in seconds (10 seconds). + static constexpr int NEGATIVE_CACHE_SECONDS = 10; + + DynamicModuleCodeCache() = default; + ~DynamicModuleCodeCache() = default; + + /** + * Looks up an entry in the cache. + * @param key The SHA256 hash key. + * @param now Current monotonic time for TTL checks. + * @return CacheLookupResult containing the lookup results. + */ + CacheLookupResult lookup(const std::string& key, MonotonicTime now); + + /** + * Marks a cache entry as in-progress for fetching. + * @param key The SHA256 hash key. + * @param now Current monotonic time. + */ + void markInProgress(const std::string& key, MonotonicTime now); + + /** + * Updates a cache entry with fetched code. + * @param key The SHA256 hash key. + * @param code The fetched module bytes (empty string indicates fetch failure). + * @param now Current monotonic time. + */ + void update(const std::string& key, const std::string& code, MonotonicTime now); + + /** + * Returns the current number of entries in the cache. + */ + size_t size() const; + + /** + * Clears the cache. Primarily for testing. + */ + void clear(); + + /** + * Sets a time offset for testing TTL behavior. + */ + void setTimeOffsetForTesting(MonotonicTime::duration offset); + +private: + // Removes expired entries during lookup. + void removeExpiredEntries(MonotonicTime now); + + mutable Thread::MutexBasicLockable mutex_; + absl::flat_hash_map cache_; + MonotonicTime::duration time_offset_for_testing_{}; +}; + +/** + * Singleton accessor for the global code cache. + */ +DynamicModuleCodeCache& getCodeCache(); + +/** + * Clears the code cache. Primarily for testing. + */ +void clearCodeCacheForTesting(); + +/** + * Sets a time offset for the code cache. Primarily for testing. + */ +void setTimeOffsetForCodeCacheForTesting(MonotonicTime::duration d); + +/** + * Adapter for remote data fetching that integrates with the code cache. + */ +class RemoteDataFetcherAdapter : public Config::DataFetcher::RemoteDataFetcherCallback, + public Event::DeferredDeletable { +public: + explicit RemoteDataFetcherAdapter(FetchCallback cb) : callback_(std::move(cb)) {} + ~RemoteDataFetcherAdapter() override = default; + + // Config::DataFetcher::RemoteDataFetcherCallback + void onSuccess(const std::string& data) override { callback_(data); } + void onFailure(Config::DataFetcher::FailureReason) override { callback_(""); } + + void setFetcher(std::unique_ptr&& fetcher) { + fetcher_ = std::move(fetcher); + } + +private: + FetchCallback callback_; + std::unique_ptr fetcher_; +}; + +} // namespace DynamicModules +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/dynamic_modules/dynamic_modules.cc b/source/extensions/dynamic_modules/dynamic_modules.cc index 69e6d189ae67d..5c25062a7c44f 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.cc +++ b/source/extensions/dynamic_modules/dynamic_modules.cc @@ -2,10 +2,15 @@ #include +#include +#include #include #include "envoy/common/exception.h" +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/hex.h" +#include "source/common/crypto/utility.h" #include "source/extensions/dynamic_modules/abi/abi.h" #include "source/extensions/dynamic_modules/abi/abi_version.h" @@ -123,6 +128,81 @@ void* DynamicModule::getSymbol(const absl::string_view symbol_ref) const { return dlsym(handle_, std::string(symbol_ref).c_str()); } +absl::StatusOr newDynamicModuleFromBytes(absl::string_view module_bytes, + absl::string_view sha256_hash, + bool do_not_close, bool load_globally) { + if (module_bytes.empty()) { + return absl::InvalidArgumentError("Module bytes cannot be empty"); + } + + // Compute SHA256 hash of the module bytes. + auto& crypto_util = Common::Crypto::UtilitySingleton::get(); + Buffer::OwnedImpl buffer{std::string{module_bytes}}; + const std::string computed_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); + + // Verify SHA256 hash if provided. + if (!sha256_hash.empty() && computed_hash != sha256_hash) { + return absl::InvalidArgumentError( + absl::StrCat("SHA256 hash mismatch: expected ", sha256_hash, ", got ", computed_hash)); + } + + // Use the hash (computed or verified) for the temp file name. + const std::string hash_for_filename = + sha256_hash.empty() ? computed_hash : std::string(sha256_hash); + + // Construct the temp file path using the hash for deduplication. + // The path format is: /tmp/envoy_dynmod_.so + const std::filesystem::path temp_dir = std::filesystem::temp_directory_path(); + const std::filesystem::path temp_file_path = + temp_dir / fmt::format("envoy_dynmod_{}.so", hash_for_filename); + + // Check if the file already exists (deduplication). + if (!std::filesystem::exists(temp_file_path)) { + // Write to a temporary file first, then atomically rename to avoid partial writes. + const std::filesystem::path temp_file_writing = + temp_dir / fmt::format("envoy_dynmod_{}.so.tmp.{}", hash_for_filename, getpid()); + + // Write the module bytes to the temp file with secure permissions. + std::ofstream ofs(temp_file_writing, std::ios::binary | std::ios::trunc); + if (!ofs) { + return absl::InternalError( + absl::StrCat("Failed to create temp file: ", temp_file_writing.string())); + } + ofs.write(module_bytes.data(), module_bytes.size()); + if (!ofs) { + std::filesystem::remove(temp_file_writing); + return absl::InternalError( + absl::StrCat("Failed to write module bytes to temp file: ", temp_file_writing.string())); + } + ofs.close(); + + // Set file permissions to 0600 (owner read/write only). + std::error_code ec; + std::filesystem::permissions( + temp_file_writing, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, + ec); + if (ec) { + std::filesystem::remove(temp_file_writing); + return absl::InternalError( + absl::StrCat("Failed to set permissions on temp file: ", ec.message())); + } + + // Atomically rename the temp file to the final path. + std::filesystem::rename(temp_file_writing, temp_file_path, ec); + if (ec) { + // If rename fails (e.g., another process created the file), that's OK - use the existing + // file. + std::filesystem::remove(temp_file_writing); + if (!std::filesystem::exists(temp_file_path)) { + return absl::InternalError(absl::StrCat("Failed to rename temp file: ", ec.message())); + } + } + } + + // Load the module from the temp file. + return newDynamicModule(temp_file_path, do_not_close, load_globally); +} + } // namespace DynamicModules } // namespace Extensions } // namespace Envoy diff --git a/source/extensions/dynamic_modules/dynamic_modules.h b/source/extensions/dynamic_modules/dynamic_modules.h index 42d7ac7124e34..1f9ed60d788a8 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.h +++ b/source/extensions/dynamic_modules/dynamic_modules.h @@ -86,6 +86,28 @@ newDynamicModule(const std::filesystem::path& object_file_absolute_path, const b absl::StatusOr newDynamicModuleByName(const absl::string_view module_name, const bool do_not_close, const bool load_globally = false); + +/** + * Creates a new DynamicModule from in-memory bytes. + * + * Since dlopen requires a file path, the bytes are written to a temporary file before loading. + * The temporary file is named using the SHA256 hash to enable deduplication across multiple + * loads of the same module content. + * + * @param module_bytes the raw bytes of the dynamic module (.so file). + * @param sha256_hash the expected SHA256 hash of the module bytes for verification. + * If empty, no verification is performed (not recommended for remote sources). + * @param do_not_close if true, the dlopen will be called with RTLD_NODELETE, so the loaded object + * will not be destroyed. + * @param load_globally if true, the dlopen will be called with RTLD_GLOBAL, so the loaded object + * can share symbols with other dynamically loaded modules. + * @return a DynamicModulePtr on success, or an error status if loading fails. + */ +absl::StatusOr newDynamicModuleFromBytes(absl::string_view module_bytes, + absl::string_view sha256_hash, + bool do_not_close, + bool load_globally = false); + } // namespace DynamicModules } // namespace Extensions } // namespace Envoy diff --git a/source/extensions/filters/http/dynamic_modules/BUILD b/source/extensions/filters/http/dynamic_modules/BUILD index 921fbd49eee28..cea2a38c9cb0d 100644 --- a/source/extensions/filters/http/dynamic_modules/BUILD +++ b/source/extensions/filters/http/dynamic_modules/BUILD @@ -38,6 +38,14 @@ envoy_cc_library( ":abi_impl", ":filter_config_lib", ":filter_lib", + "//envoy/init:manager_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:hex_lib", + "//source/common/config:datasource_lib", + "//source/common/crypto:utility_lib", + "//source/extensions/common/wasm:remote_async_datasource_lib", + "//source/extensions/dynamic_modules:code_cache_lib", + "//source/extensions/dynamic_modules:dynamic_modules_lib", "//source/extensions/filters/http/common:factory_base_lib", "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", ], diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 594cce6a647ad..e5b215d024d09 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -1,5 +1,10 @@ #include "source/extensions/filters/http/dynamic_modules/factory.h" +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/hex.h" +#include "source/common/config/datasource.h" +#include "source/common/crypto/utility.h" +#include "source/extensions/dynamic_modules/code_cache.h" #include "source/extensions/filters/http/dynamic_modules/filter.h" #include "source/extensions/filters/http/dynamic_modules/filter_config.h" @@ -7,11 +12,83 @@ namespace Envoy { namespace Server { namespace Configuration { +namespace { + +// Helper function to load a module from bytes and create the filter config. +absl::StatusOr< + Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr> +createFilterConfigFromBytes(absl::string_view module_bytes, absl::string_view sha256_hash, + const FilterConfig& proto_config, + Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope) { + const auto& module_config = proto_config.dynamic_module_config(); + + auto dynamic_module = Extensions::DynamicModules::newDynamicModuleFromBytes( + module_bytes, sha256_hash, module_config.do_not_close(), module_config.load_globally()); + if (!dynamic_module.ok()) { + return absl::InvalidArgumentError("Failed to load dynamic module from bytes: " + + std::string(dynamic_module.status().message())); + } + + std::string config; + if (proto_config.has_filter_config()) { + auto config_or_error = MessageUtil::anyToBytes(proto_config.filter_config()); + if (!config_or_error.ok()) { + return config_or_error.status(); + } + config = std::move(config_or_error.value()); + } + + const std::string metrics_namespace = + module_config.metrics_namespace().empty() + ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) + : module_config.metrics_namespace(); + + return Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( + proto_config.filter_name(), config, metrics_namespace, proto_config.terminal_filter(), + std::move(dynamic_module.value()), scope, context); +} + +// Helper to create the filter factory callback from a filter config. +Http::FilterFactoryCb createFilterFactoryCallback( + Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr + filter_config) { + return [config = std::move(filter_config)](Http::FilterChainFactoryCallbacks& callbacks) -> void { + const std::string& worker_name = callbacks.dispatcher().name(); + auto pos = worker_name.find_first_of('_'); + ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); + uint32_t worker_index; + if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { + IS_ENVOY_BUG("failed to parse worker index from name"); + } + auto filter = + std::make_shared( + config, config->stats_scope_->symbolTable(), worker_index); + filter->initializeInModuleFilter(); + callbacks.addStreamFilter(filter); + }; +} + +} // namespace + absl::StatusOr DynamicModuleConfigFactory::createFilterFactory( const FilterConfig& proto_config, const std::string&, - Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope) { + Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, + Init::Manager* init_manager) { const auto& module_config = proto_config.dynamic_module_config(); + + // Check if the new 'module' field is set. + if (module_config.has_module()) { + return createFilterFactoryFromAsyncDataSource(proto_config, context, scope, init_manager); + } + + // Legacy path: load module by name. + if (module_config.name().empty()) { + return absl::InvalidArgumentError( + "Either 'name' or 'module' must be specified in dynamic_module_config"); + } + auto dynamic_module = Extensions::DynamicModules::newDynamicModuleByName( module_config.name(), module_config.do_not_close(), module_config.load_globally()); if (!dynamic_module.ok()) { @@ -46,20 +123,217 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - return [config = filter_config.value()](Http::FilterChainFactoryCallbacks& callbacks) -> void { - const std::string& worker_name = callbacks.dispatcher().name(); - auto pos = worker_name.find_first_of('_'); - ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); - uint32_t worker_index; - if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { - IS_ENVOY_BUG("failed to parse worker index from name"); + return createFilterFactoryCallback(filter_config.value()); +} + +absl::StatusOr +DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( + const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope, Init::Manager* init_manager) { + + const auto& module_config = proto_config.dynamic_module_config(); + const auto& async_source = module_config.module(); + + // Use configured metrics namespace or fall back to the default. + const std::string metrics_namespace = + module_config.metrics_namespace().empty() + ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) + : module_config.metrics_namespace(); + + if (async_source.has_local()) { + // Synchronous path: local file or inline bytes. + auto data_or_error = Config::DataSource::read(async_source.local(), true, context.api()); + if (!data_or_error.ok()) { + return absl::InvalidArgumentError("Failed to read module data: " + + std::string(data_or_error.status().message())); } - auto filter = - std::make_shared( - config, config->stats_scope_->symbolTable(), worker_index); - filter->initializeInModuleFilter(); - callbacks.addStreamFilter(filter); - }; + + const std::string& module_bytes = data_or_error.value(); + if (module_bytes.empty()) { + return absl::InvalidArgumentError("Module data is empty"); + } + + // Compute SHA256 for the local data (no verification needed, just for temp file naming). + auto filter_config = + createFilterConfigFromBytes(module_bytes, "", proto_config, context, scope); + if (!filter_config.ok()) { + return filter_config.status(); + } + + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + return createFilterFactoryCallback(filter_config.value()); + } + + if (async_source.has_remote()) { + // Asynchronous path: remote HTTP fetch. + const auto& remote_source = async_source.remote(); + const std::string& sha256_hash = remote_source.sha256(); + + if (sha256_hash.empty()) { + return absl::InvalidArgumentError("SHA256 hash is required for remote module sources"); + } + + // Check the code cache first. + auto& code_cache = Extensions::DynamicModules::getCodeCache(); + auto now = context.mainThreadDispatcher().timeSource().monotonicTime(); + auto cache_result = code_cache.lookup(sha256_hash, now); + + if (cache_result.cache_hit && !cache_result.code.empty()) { + // Cache hit with valid code - load synchronously. + auto filter_config = + createFilterConfigFromBytes(cache_result.code, sha256_hash, proto_config, context, scope); + if (!filter_config.ok()) { + return filter_config.status(); + } + + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + return createFilterFactoryCallback(filter_config.value()); + } + + if (cache_result.cache_hit && cache_result.code.empty()) { + // Negative cache hit (recent fetch failure). + if (module_config.nack_on_module_cache_miss()) { + return absl::UnavailableError( + "Module fetch recently failed (negative cache hit), NACK'ing configuration"); + } + // For warming mode with negative cache, we still need to try fetching again. + } + + if (cache_result.fetch_in_progress) { + // Another fetch is already in progress. + if (module_config.nack_on_module_cache_miss()) { + return absl::UnavailableError("Module fetch in progress, NACK'ing configuration"); + } + // For warming mode, we'd need to wait - but this is complex to implement. + // For now, treat as unavailable. + return absl::UnavailableError("Module fetch in progress"); + } + + // Need to fetch the module. + if (module_config.nack_on_module_cache_miss()) { + // NACK mode: Start background fetch and reject this config. + code_cache.markInProgress(sha256_hash, now); + + // Create a holder structure that keeps both adapter and fetcher alive together. + struct FetchHolder : public Event::DeferredDeletable { + Extensions::DynamicModules::RemoteDataFetcherAdapter adapter; + std::unique_ptr fetcher; + + FetchHolder(Extensions::DynamicModules::FetchCallback cb) : adapter(std::move(cb)) {} + ~FetchHolder() override = default; + }; + + auto holder = std::make_unique([sha256_hash, &context](const std::string& data) { + auto& cache = Extensions::DynamicModules::getCodeCache(); + auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); + + if (!data.empty()) { + // Verify SHA256. + Buffer::OwnedImpl buffer(data); + auto& crypto_util = Common::Crypto::UtilitySingleton::get(); + const std::string computed_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); + if (computed_hash == sha256_hash) { + cache.update(sha256_hash, data, fetch_time); + return; + } + // Hash mismatch - treat as failure. + ENVOY_LOG_MISC(warn, "Dynamic module SHA256 mismatch: expected {}, got {}", sha256_hash, + computed_hash); + } + // Fetch failed or hash mismatch - update with empty data for negative caching. + cache.update(sha256_hash, "", fetch_time); + }); + + holder->fetcher = std::make_unique( + context.clusterManager(), remote_source.http_uri(), sha256_hash, holder->adapter); + holder->fetcher->fetch(); + + // Defer deletion of the holder to ensure it lives until the callback is invoked. + context.mainThreadDispatcher().deferredDelete(std::move(holder)); + + return absl::UnavailableError( + "Remote module not in cache, background fetch started, NACK'ing configuration"); + } + + // Warming mode: Use init manager to block until fetch completes. + if (init_manager == nullptr) { + return absl::InvalidArgumentError( + "Init manager required for warming mode with remote module sources"); + } + + // Mark as in progress. + code_cache.markInProgress(sha256_hash, now); + + // Create a shared state to hold the fetched data, filter config, and keep the provider alive. + struct AsyncLoadState { + std::string module_bytes; + Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr filter_config; + RemoteAsyncDataProviderPtr remote_provider; + bool fetch_completed{false}; + bool fetch_success{false}; + }; + auto state = std::make_shared(); + + // Use RemoteAsyncDataProvider for the fetch. + // The callback will be invoked when the fetch completes. + state->remote_provider = std::make_unique( + context.clusterManager(), *init_manager, remote_source, context.mainThreadDispatcher(), + context.api().randomGenerator(), false, + [state, sha256_hash, proto_config_copy = proto_config, &context, &scope, + metrics_namespace](const std::string& data) { + auto& cache = Extensions::DynamicModules::getCodeCache(); + auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); + + state->fetch_completed = true; + if (!data.empty()) { + // Verify SHA256. + Buffer::OwnedImpl buffer(data); + auto& crypto_util = Common::Crypto::UtilitySingleton::get(); + const std::string computed_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); + if (computed_hash == sha256_hash) { + state->module_bytes = data; + state->fetch_success = true; + cache.update(sha256_hash, data, fetch_time); + + // Now create the filter config. + auto filter_config = + createFilterConfigFromBytes(data, sha256_hash, proto_config_copy, context, scope); + if (filter_config.ok()) { + state->filter_config = filter_config.value(); + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + } + return; + } + ENVOY_LOG_MISC(warn, "Dynamic module SHA256 mismatch: expected {}, got {}", sha256_hash, + computed_hash); + } + cache.update(sha256_hash, "", fetch_time); + }); + + // Return a factory callback that uses the async-loaded config. + return [state](Http::FilterChainFactoryCallbacks& callbacks) -> void { + if (!state->filter_config) { + // Module failed to load - skip adding filter (fail open behavior for warming mode). + return; + } + + const std::string& worker_name = callbacks.dispatcher().name(); + auto pos = worker_name.find_first_of('_'); + ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); + uint32_t worker_index; + if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { + IS_ENVOY_BUG("failed to parse worker index from name"); + } + auto filter = + std::make_shared( + state->filter_config, state->filter_config->stats_scope_->symbolTable(), + worker_index); + filter->initializeInModuleFilter(); + callbacks.addStreamFilter(filter); + }; + } + + return absl::InvalidArgumentError("Invalid AsyncDataSource: neither local nor remote specified"); } Envoy::Http::FilterFactoryCb diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index 825033c3523b9..18bfb524da33a 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -1,9 +1,14 @@ #pragma once +#include +#include + #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.validate.h" +#include "envoy/init/manager.h" #include "envoy/server/filter_config.h" +#include "source/extensions/common/wasm/remote_async_datasource.h" #include "source/extensions/dynamic_modules/dynamic_modules.h" #include "source/extensions/filters/http/common/factory_base.h" @@ -23,7 +28,8 @@ class DynamicModuleConfigFactory createFilterFactoryFromProtoTyped(const FilterConfig& proto_config, const std::string& stat_prefix, DualInfo dual_info, Server::Configuration::ServerFactoryContext& context) override { - return createFilterFactory(proto_config, stat_prefix, context, dual_info.scope); + return createFilterFactory(proto_config, stat_prefix, context, dual_info.scope, + &dual_info.init_manager); } Envoy::Http::FilterFactoryCb createFilterFactoryFromProtoWithServerContextTyped( const FilterConfig& proto_config, const std::string& stat_prefix, @@ -31,7 +37,13 @@ class DynamicModuleConfigFactory absl::StatusOr createFilterFactory(const FilterConfig& proto_config, const std::string& stat_prefix, - Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope); + Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, + Init::Manager* init_manager = nullptr); + + absl::StatusOr + createFilterFactoryFromAsyncDataSource(const FilterConfig& proto_config, + Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope, Init::Manager* init_manager); absl::StatusOr createRouteSpecificFilterConfigTyped(const RouteConfigProto&, diff --git a/test/extensions/dynamic_modules/BUILD b/test/extensions/dynamic_modules/BUILD index c5c71b9db4dc8..e5fec93a27484 100644 --- a/test/extensions/dynamic_modules/BUILD +++ b/test/extensions/dynamic_modules/BUILD @@ -37,6 +37,9 @@ envoy_cc_test( rbe_pool = "6gig", deps = [ ":util", + "//source/common/buffer:buffer_lib", + "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", "//source/extensions/dynamic_modules:abi_impl", "//source/extensions/dynamic_modules:dynamic_modules_lib", ], @@ -67,6 +70,16 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "code_cache_test", + srcs = ["code_cache_test.cc"], + deps = [ + "//source/extensions/dynamic_modules:code_cache_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test_library( name = "util", srcs = ["util.cc"], diff --git a/test/extensions/dynamic_modules/code_cache_test.cc b/test/extensions/dynamic_modules/code_cache_test.cc new file mode 100644 index 0000000000000..3db6be1a8e2e8 --- /dev/null +++ b/test/extensions/dynamic_modules/code_cache_test.cc @@ -0,0 +1,179 @@ +#include "source/extensions/dynamic_modules/code_cache.h" + +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace DynamicModules { + +class CodeCacheTest : public testing::Test { +protected: + void SetUp() override { clearCodeCacheForTesting(); } + + void TearDown() override { clearCodeCacheForTesting(); } +}; + +TEST_F(CodeCacheTest, LookupMiss) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + auto result = cache.lookup("nonexistent_key", now); + EXPECT_FALSE(result.cache_hit); + EXPECT_FALSE(result.fetch_in_progress); + EXPECT_TRUE(result.code.empty()); +} + +TEST_F(CodeCacheTest, MarkInProgressAndLookup) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.markInProgress("test_key", now); + EXPECT_EQ(cache.size(), 1); + + auto result = cache.lookup("test_key", now); + EXPECT_TRUE(result.cache_hit); + EXPECT_TRUE(result.fetch_in_progress); + EXPECT_TRUE(result.code.empty()); +} + +TEST_F(CodeCacheTest, UpdateWithCodeAndLookup) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.markInProgress("test_key", now); + cache.update("test_key", "module_binary_data", now); + + auto result = cache.lookup("test_key", now); + EXPECT_TRUE(result.cache_hit); + EXPECT_FALSE(result.fetch_in_progress); + EXPECT_EQ(result.code, "module_binary_data"); +} + +TEST_F(CodeCacheTest, NegativeCaching) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + // Update with empty code (failure). + cache.update("test_key", "", now); + + // Lookup within negative cache TTL. + auto result = cache.lookup("test_key", now); + EXPECT_TRUE(result.cache_hit); + EXPECT_FALSE(result.fetch_in_progress); + EXPECT_TRUE(result.code.empty()); + + // Lookup after negative cache TTL expires (10 seconds). + MonotonicTime after_expiry = now + std::chrono::seconds(11); + result = cache.lookup("test_key", after_expiry); + EXPECT_FALSE(result.cache_hit); + EXPECT_FALSE(result.fetch_in_progress); + EXPECT_TRUE(result.code.empty()); +} + +TEST_F(CodeCacheTest, PositiveCacheTTL) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.update("test_key", "module_binary_data", now); + + // Lookup within cache TTL (24 hours). + MonotonicTime within_ttl = now + std::chrono::hours(23); + auto result = cache.lookup("test_key", within_ttl); + EXPECT_TRUE(result.cache_hit); + EXPECT_EQ(result.code, "module_binary_data"); + + // Lookup after cache TTL expires. + MonotonicTime after_ttl = now + std::chrono::hours(25); + result = cache.lookup("test_key", after_ttl); + EXPECT_FALSE(result.cache_hit); + EXPECT_TRUE(result.code.empty()); +} + +TEST_F(CodeCacheTest, MultipleEntries) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.update("key1", "data1", now); + cache.update("key2", "data2", now); + cache.update("key3", "data3", now); + + EXPECT_EQ(cache.size(), 3); + + auto result1 = cache.lookup("key1", now); + EXPECT_TRUE(result1.cache_hit); + EXPECT_EQ(result1.code, "data1"); + + auto result2 = cache.lookup("key2", now); + EXPECT_TRUE(result2.cache_hit); + EXPECT_EQ(result2.code, "data2"); + + auto result3 = cache.lookup("key3", now); + EXPECT_TRUE(result3.cache_hit); + EXPECT_EQ(result3.code, "data3"); +} + +TEST_F(CodeCacheTest, Clear) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.update("key1", "data1", now); + cache.update("key2", "data2", now); + EXPECT_EQ(cache.size(), 2); + + cache.clear(); + EXPECT_EQ(cache.size(), 0); + + auto result = cache.lookup("key1", now); + EXPECT_FALSE(result.cache_hit); +} + +TEST_F(CodeCacheTest, GlobalCacheAccessor) { + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + auto& cache1 = getCodeCache(); + cache1.update("global_key", "global_data", now); + + auto& cache2 = getCodeCache(); + auto result = cache2.lookup("global_key", now); + EXPECT_TRUE(result.cache_hit); + EXPECT_EQ(result.code, "global_data"); + + // Both should be the same instance. + EXPECT_EQ(&cache1, &cache2); +} + +TEST_F(CodeCacheTest, InProgressDoesNotExpire) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.markInProgress("test_key", now); + + // Even after a long time, in-progress entries should not be removed. + MonotonicTime much_later = now + std::chrono::hours(100); + auto result = cache.lookup("test_key", much_later); + EXPECT_TRUE(result.cache_hit); + EXPECT_TRUE(result.fetch_in_progress); +} + +TEST_F(CodeCacheTest, UpdateClearsInProgress) { + DynamicModuleCodeCache cache; + MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); + + cache.markInProgress("test_key", now); + + auto result = cache.lookup("test_key", now); + EXPECT_TRUE(result.fetch_in_progress); + + cache.update("test_key", "module_data", now); + + result = cache.lookup("test_key", now); + EXPECT_FALSE(result.fetch_in_progress); + EXPECT_EQ(result.code, "module_data"); +} + +} // namespace DynamicModules +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/dynamic_modules/dynamic_modules_test.cc b/test/extensions/dynamic_modules/dynamic_modules_test.cc index dece7e15f3693..0db247a2f86e9 100644 --- a/test/extensions/dynamic_modules/dynamic_modules_test.cc +++ b/test/extensions/dynamic_modules/dynamic_modules_test.cc @@ -1,3 +1,8 @@ +#include + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/hex.h" +#include "source/common/crypto/utility.h" #include "source/extensions/dynamic_modules/dynamic_modules.h" #include "test/extensions/dynamic_modules/util.h" @@ -168,6 +173,98 @@ TEST(CreateDynamicModulesByName, ModuleNotFound) { "Failed to load dynamic module: libno_op.so not found in any search path")); } +// Tests for newDynamicModuleFromBytes + +TEST(CreateDynamicModulesFromBytes, EmptyBytes) { + absl::StatusOr module = newDynamicModuleFromBytes("", "", false); + EXPECT_FALSE(module.ok()); + EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(module.status().message(), testing::HasSubstr("Module bytes cannot be empty")); +} + +TEST(CreateDynamicModulesFromBytes, ValidModuleNoHash) { + // Read a valid module from disk. + std::string module_path = testSharedObjectPath("no_op", "c"); + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + // Load the module from bytes without providing a hash. + absl::StatusOr module = + newDynamicModuleFromBytes(module_bytes, "", false, false); + EXPECT_TRUE(module.ok()) << "Failed to load module: " << module.status().message(); +} + +TEST(CreateDynamicModulesFromBytes, ValidModuleWithCorrectHash) { + // Read a valid module from disk. + std::string module_path = testSharedObjectPath("no_op", "c"); + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + // Compute the expected SHA256 hash. + auto& crypto_util = Common::Crypto::UtilitySingleton::get(); + Buffer::OwnedImpl buffer(module_bytes); + std::string expected_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); + + // Load the module from bytes with the correct hash. + absl::StatusOr module = + newDynamicModuleFromBytes(module_bytes, expected_hash, false, false); + EXPECT_TRUE(module.ok()) << "Failed to load module: " << module.status().message(); +} + +TEST(CreateDynamicModulesFromBytes, ValidModuleWithIncorrectHash) { + // Read a valid module from disk. + std::string module_path = testSharedObjectPath("no_op", "c"); + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + // Try to load the module with an incorrect hash. + absl::StatusOr module = + newDynamicModuleFromBytes(module_bytes, "incorrect_hash", false, false); + EXPECT_FALSE(module.ok()); + EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(module.status().message(), testing::HasSubstr("SHA256 hash mismatch")); +} + +TEST(CreateDynamicModulesFromBytes, TempFileDeduplication) { + // Read a valid module from disk. + std::string module_path = testSharedObjectPath("no_op", "c"); + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + // Load the module twice with the same bytes. + absl::StatusOr module1 = + newDynamicModuleFromBytes(module_bytes, "", false, false); + EXPECT_TRUE(module1.ok()) << "Failed to load module1: " << module1.status().message(); + + absl::StatusOr module2 = + newDynamicModuleFromBytes(module_bytes, "", false, false); + EXPECT_TRUE(module2.ok()) << "Failed to load module2: " << module2.status().message(); + + // Both should succeed and point to the same underlying module (via dlopen deduplication). +} + +TEST(CreateDynamicModulesFromBytes, InvalidModuleBytes) { + // Try to load invalid bytes as a module. + std::string invalid_bytes = "this is not a valid shared object"; + absl::StatusOr module = + newDynamicModuleFromBytes(invalid_bytes, "", false, false); + EXPECT_FALSE(module.ok()); + // The error should come from dlopen failing to load the invalid file. + EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); +} + } // namespace DynamicModules } // namespace Extensions } // namespace Envoy diff --git a/test/extensions/dynamic_modules/test_data/c/BUILD b/test/extensions/dynamic_modules/test_data/c/BUILD index 7a39f9d39d8ba..63077b0222684 100644 --- a/test/extensions/dynamic_modules/test_data/c/BUILD +++ b/test/extensions/dynamic_modules/test_data/c/BUILD @@ -10,6 +10,7 @@ package(default_visibility = [ "//test/extensions/dynamic_modules/listener:__pkg__", "//test/extensions/dynamic_modules/network:__pkg__", "//test/extensions/dynamic_modules/udp:__pkg__", + "//test/extensions/filters/http/dynamic_modules:__pkg__", ]) test_program(name = "no_op") diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD new file mode 100644 index 0000000000000..9fe79ff9aedfd --- /dev/null +++ b/test/extensions/filters/http/dynamic_modules/BUILD @@ -0,0 +1,35 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/c:no_op", + ], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/common/common:base64_lib", + "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", + "//source/common/http:message_lib", + "//source/common/stats:isolated_store_lib", + "//source/extensions/dynamic_modules:code_cache_lib", + "//source/extensions/filters/http/dynamic_modules:factory_lib", + "//test/extensions/dynamic_modules:util", + "//test/mocks/http:http_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/server:server_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc new file mode 100644 index 0000000000000..c454d9273fce3 --- /dev/null +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -0,0 +1,278 @@ +#include +#include + +#include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" +#include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.validate.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/base64.h" +#include "source/common/common/hex.h" +#include "source/common/crypto/utility.h" +#include "source/common/http/message_impl.h" +#include "source/common/stats/isolated_store_impl.h" +#include "source/extensions/dynamic_modules/code_cache.h" +#include "source/extensions/filters/http/dynamic_modules/factory.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::ReturnRef; + +namespace Envoy { +namespace Server { +namespace Configuration { + +class DynamicModuleFilterConfigTest : public Event::TestUsingSimulatedTime, public testing::Test { +protected: + DynamicModuleFilterConfigTest() : api_(Api::createApiForTest(stats_store_)) { + ON_CALL(context_.server_factory_context_, api()).WillByDefault(ReturnRef(*api_)); + ON_CALL(context_, scope()).WillByDefault(ReturnRef(stats_scope_)); + ON_CALL(context_, listenerInfo()).WillByDefault(ReturnRef(listener_info_)); + ON_CALL(listener_info_, metadata()).WillByDefault(ReturnRef(listener_metadata_)); + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager_)); + ON_CALL(context_.server_factory_context_, clusterManager()) + .WillByDefault(ReturnRef(cluster_manager_)); + ON_CALL(context_.server_factory_context_, mainThreadDispatcher()) + .WillByDefault(ReturnRef(dispatcher_)); + } + + void SetUp() override { Extensions::DynamicModules::clearCodeCacheForTesting(); } + + void initializeForRemote() { + retry_timer_ = new Event::MockTimer(); + + EXPECT_CALL(dispatcher_, createTimer_(_)) + .WillOnce(testing::Invoke([this](Event::TimerCb timer_cb) { + retry_timer_cb_ = timer_cb; + return retry_timer_; + })); + } + + NiceMock listener_info_; + Stats::IsolatedStoreImpl stats_store_; + Stats::Scope& stats_scope_{*stats_store_.rootScope()}; + Api::ApiPtr api_; + envoy::config::core::v3::Metadata listener_metadata_; + Init::ManagerImpl init_manager_{"init_manager"}; + NiceMock cluster_manager_; + Init::ExpectableWatcherImpl init_watcher_; + NiceMock dispatcher_; + Event::MockTimer* retry_timer_; + Event::TimerCb retry_timer_cb_; + + NiceMock context_; +}; + +TEST_F(DynamicModuleFilterConfigTest, LegacyNameBasedLoading) { + // Set up the search path to find the test module. + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), + 1); + + const std::string yaml = R"EOF( + dynamic_module_config: + name: "no_op" + do_not_close: true + filter_name: "test_filter" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + TestEnvironment::unsetEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH"); +} + +TEST_F(DynamicModuleFilterConfigTest, LocalFileLoading) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + dynamic_module_config: + module: + local: + filename: ")EOF", + module_path, R"EOF(" + do_not_close: true + filter_name: "test_filter" + )EOF")); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); +} + +TEST_F(DynamicModuleFilterConfigTest, InlineBytesLoading) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + local: + inline_bytes: ")EOF", + Base64::encode(module_bytes.data(), module_bytes.size()), + R"EOF(" + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); +} + +// Remote loading tests are covered by RemoteLoadingNackOnCacheMiss which tests the +// fetch and cache mechanism. The warming mode path is complex due to stats lifecycle +// issues and is not tested here. Integration tests should cover the full flow. + +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackOnCacheMiss) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + nack_on_module_cache_miss: true + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::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().add(module_bytes); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + DynamicModuleConfigFactory factory; + // First attempt should fail with NACK. + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Second attempt should succeed from cache. + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + EXPECT_EQ(init_manager2.state(), Init::Manager::State::Initialized); + + dispatcher_.clearDeferredDeleteList(); +} + +// Note: RemoteMissingSha256 test is not included because the proto validation +// already enforces that sha256 must be non-empty for remote sources. The validation +// happens during config parsing, not in our factory code. + +TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { + const std::string yaml = R"EOF( + dynamic_module_config: + do_not_close: true + filter_name: "test_filter" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), + testing::HasSubstr("Either 'name' or 'module' must be specified")); +} + +TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { + const std::string yaml = R"EOF( + dynamic_module_config: + module: + local: + filename: "/nonexistent/path/to/module.so" + do_not_close: true + filter_name: "test_filter" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to read module data")); +} + +} // namespace Configuration +} // namespace Server +} // namespace Envoy From 810558e012da698537b1480c3f264aba1e211c16 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Thu, 5 Feb 2026 17:44:05 +0530 Subject: [PATCH 02/21] add tests and cleanup Signed-off-by: Anurag Aggarwal --- .../dynamic_modules/v3/dynamic_modules.proto | 46 +- source/extensions/dynamic_modules/BUILD | 11 +- .../dynamic_modules/dynamic_modules.cc | 16 +- .../dynamic_modules/dynamic_modules.h | 19 +- .../{code_cache.cc => module_cache.cc} | 68 +-- .../{code_cache.h => module_cache.h} | 56 +-- .../filters/http/dynamic_modules/BUILD | 5 +- .../filters/http/dynamic_modules/factory.cc | 154 +++---- .../filters/http/dynamic_modules/factory.h | 1 - test/extensions/dynamic_modules/BUILD | 6 +- ...ode_cache_test.cc => module_cache_test.cc} | 76 ++-- .../filters/http/dynamic_modules/BUILD | 2 +- .../http/dynamic_modules/config_test.cc | 430 +++++++++++++++++- 13 files changed, 588 insertions(+), 302 deletions(-) rename source/extensions/dynamic_modules/{code_cache.cc => module_cache.cc} (58%) rename source/extensions/dynamic_modules/{code_cache.h => module_cache.h} (65%) rename test/extensions/dynamic_modules/{code_cache_test.cc => module_cache_test.cc} (71%) diff --git a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto index dbaa2e67e6067..fd4981752d2a3 100644 --- a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto +++ b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto @@ -86,45 +86,25 @@ message DynamicModuleConfig { // Defaults to ``dynamicmodulescustom``. string metrics_namespace = 5; - // The dynamic module binary to load. + // The dynamic module binary to load. Supports local file paths (via ``local.filename``), + // inline bytes (via ``local.inline_bytes``), or remote HTTP URLs (via ``remote``). // - // This field supports loading modules from: - // - // * Local file path (via ``local.filename``) - // * Inline bytes (via ``local.inline_bytes`` or ``local.inline_string``) - // * Remote HTTP URL (via ``remote``) + // For remote sources, the ``sha256`` field is required and is used both for integrity + // verification and as the cache key. Fetched modules are cached in-memory for 24 hours; + // failed fetches are negatively cached for 10 seconds to avoid retry storms. // // When both ``name`` and ``module`` are set, ``module`` takes precedence. - // - // Example configurations: - // - // Local file: - // - // .. code-block:: yaml - // - // module: - // local: - // filename: "/path/to/libmy_module.so" - // - // Remote HTTP: - // - // .. code-block:: yaml - // - // module: - // remote: - // http_uri: - // uri: "https://modules.example.com/libmy_module.so" - // cluster: module_server - // timeout: 10s - // sha256: "abc123..." - // config.core.v3.AsyncDataSource module = 6; - // If true and the module needs to be remotely fetched and it is not in the cache, - // then NACK the configuration update and do a background fetch to fill the cache. - // If false, fetch the module asynchronously and enter warming state. + // Controls how a cache miss for a remote module is handled. + // + // When true (NACK mode), a cache miss causes an immediate NACK of the xDS config update. + // A background fetch is started and the module will be available on the next config push. + // + // When false (warming mode), the server blocks during initialization until the fetch + // completes or exhausts retries. // - // This only applies when using remote data sources via the ``module`` field. + // Only applies to remote data sources via the ``module`` field. // // Defaults to ``false``. bool nack_on_module_cache_miss = 7; diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index c39730296c9a4..37a61cb5dede8 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -25,20 +25,15 @@ envoy_cc_library( ) envoy_cc_library( - name = "code_cache_lib", - srcs = ["code_cache.cc"], + name = "module_cache_lib", + srcs = ["module_cache.cc"], hdrs = [ - "code_cache.h", + "module_cache.h", ], deps = [ "//envoy/event:deferred_deletable", - "//envoy/event:dispatcher_interface", - "//envoy/init:manager_interface", - "//envoy/stats:stats_interface", - "//envoy/upstream:cluster_manager_interface", "//source/common/common:logger_lib", "//source/common/config:remote_data_fetcher_lib", - "//source/common/init:target_lib", "@com_google_absl//absl/container:flat_hash_map", ], ) diff --git a/source/extensions/dynamic_modules/dynamic_modules.cc b/source/extensions/dynamic_modules/dynamic_modules.cc index 5c25062a7c44f..df0fc94977848 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.cc +++ b/source/extensions/dynamic_modules/dynamic_modules.cc @@ -2,7 +2,6 @@ #include -#include #include #include @@ -146,21 +145,16 @@ absl::StatusOr newDynamicModuleFromBytes(absl::string_view mod absl::StrCat("SHA256 hash mismatch: expected ", sha256_hash, ", got ", computed_hash)); } - // Use the hash (computed or verified) for the temp file name. - const std::string hash_for_filename = - sha256_hash.empty() ? computed_hash : std::string(sha256_hash); - - // Construct the temp file path using the hash for deduplication. - // The path format is: /tmp/envoy_dynmod_.so + // Use computed_hash for the temp file name (if sha256_hash was provided and matched, + // computed_hash == sha256_hash at this point). const std::filesystem::path temp_dir = std::filesystem::temp_directory_path(); const std::filesystem::path temp_file_path = - temp_dir / fmt::format("envoy_dynmod_{}.so", hash_for_filename); + temp_dir / fmt::format("envoy_dynmod_{}.so", computed_hash); - // Check if the file already exists (deduplication). if (!std::filesystem::exists(temp_file_path)) { - // Write to a temporary file first, then atomically rename to avoid partial writes. + // Write to a pid-suffixed temp file first, then atomically rename to avoid partial reads. const std::filesystem::path temp_file_writing = - temp_dir / fmt::format("envoy_dynmod_{}.so.tmp.{}", hash_for_filename, getpid()); + temp_dir / fmt::format("envoy_dynmod_{}.so.tmp.{}", computed_hash, getpid()); // Write the module bytes to the temp file with secure permissions. std::ofstream ofs(temp_file_writing, std::ios::binary | std::ios::trunc); diff --git a/source/extensions/dynamic_modules/dynamic_modules.h b/source/extensions/dynamic_modules/dynamic_modules.h index 1f9ed60d788a8..65e5c8dc7b593 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.h +++ b/source/extensions/dynamic_modules/dynamic_modules.h @@ -88,20 +88,13 @@ absl::StatusOr newDynamicModuleByName(const absl::string_view const bool load_globally = false); /** - * Creates a new DynamicModule from in-memory bytes. - * - * Since dlopen requires a file path, the bytes are written to a temporary file before loading. - * The temporary file is named using the SHA256 hash to enable deduplication across multiple - * loads of the same module content. - * + * Creates a new DynamicModule from in-memory bytes. The bytes are written to a SHA256-named + * temporary file (for deduplication) before loading via dlopen. * @param module_bytes the raw bytes of the dynamic module (.so file). - * @param sha256_hash the expected SHA256 hash of the module bytes for verification. - * If empty, no verification is performed (not recommended for remote sources). - * @param do_not_close if true, the dlopen will be called with RTLD_NODELETE, so the loaded object - * will not be destroyed. - * @param load_globally if true, the dlopen will be called with RTLD_GLOBAL, so the loaded object - * can share symbols with other dynamically loaded modules. - * @return a DynamicModulePtr on success, or an error status if loading fails. + * @param sha256_hash the expected SHA256 hash for verification. If empty, hash is computed but + * not verified. + * @param do_not_close if true, uses RTLD_NODELETE. + * @param load_globally if true, uses RTLD_GLOBAL. */ absl::StatusOr newDynamicModuleFromBytes(absl::string_view module_bytes, absl::string_view sha256_hash, diff --git a/source/extensions/dynamic_modules/code_cache.cc b/source/extensions/dynamic_modules/module_cache.cc similarity index 58% rename from source/extensions/dynamic_modules/code_cache.cc rename to source/extensions/dynamic_modules/module_cache.cc index 1c9bc68dfa756..814a466c02489 100644 --- a/source/extensions/dynamic_modules/code_cache.cc +++ b/source/extensions/dynamic_modules/module_cache.cc @@ -1,4 +1,4 @@ -#include "source/extensions/dynamic_modules/code_cache.h" +#include "source/extensions/dynamic_modules/module_cache.h" #include "source/common/common/lock_guard.h" #include "source/common/common/thread.h" @@ -8,34 +8,34 @@ namespace Extensions { namespace DynamicModules { namespace { -// Global code cache instance. -std::unique_ptr global_code_cache; -Thread::MutexBasicLockable global_code_cache_mutex; +// Global module cache instance. +std::unique_ptr global_module_cache; +Thread::MutexBasicLockable global_module_cache_mutex; } // namespace -DynamicModuleCodeCache& getCodeCache() { - Thread::LockGuard guard(global_code_cache_mutex); - if (!global_code_cache) { - global_code_cache = std::make_unique(); +DynamicModuleCache& getModuleCache() { + Thread::LockGuard guard(global_module_cache_mutex); + if (!global_module_cache) { + global_module_cache = std::make_unique(); } - return *global_code_cache; + return *global_module_cache; } -void clearCodeCacheForTesting() { - Thread::LockGuard guard(global_code_cache_mutex); - if (global_code_cache) { - global_code_cache->clear(); +void clearModuleCacheForTesting() { + Thread::LockGuard guard(global_module_cache_mutex); + if (global_module_cache) { + global_module_cache->clear(); } } -void setTimeOffsetForCodeCacheForTesting(MonotonicTime::duration d) { - Thread::LockGuard guard(global_code_cache_mutex); - if (global_code_cache) { - global_code_cache->setTimeOffsetForTesting(d); +void setTimeOffsetForModuleCacheForTesting(MonotonicTime::duration d) { + Thread::LockGuard guard(global_module_cache_mutex); + if (global_module_cache) { + global_module_cache->setTimeOffsetForTesting(d); } } -CacheLookupResult DynamicModuleCodeCache::lookup(const std::string& key, MonotonicTime now) { +CacheLookupResult DynamicModuleCache::lookup(const std::string& key, MonotonicTime now) { Thread::LockGuard guard(mutex_); // Apply time offset for testing. @@ -49,15 +49,15 @@ CacheLookupResult DynamicModuleCodeCache::lookup(const std::string& key, Monoton return CacheLookupResult{"", false, false}; } - CodeCacheEntry& entry = it->second; + ModuleCacheEntry& entry = it->second; entry.use_time = now; if (entry.in_progress) { return CacheLookupResult{"", true, true}; } - // Check if this is a negative cache entry (empty code with recent fetch). - if (entry.code.empty()) { + // Check if this is a negative cache entry (empty module with recent fetch). + if (entry.module.empty()) { auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); if (elapsed < NEGATIVE_CACHE_SECONDS) { // Still within negative cache TTL - return empty but mark as cache hit. @@ -75,51 +75,51 @@ CacheLookupResult DynamicModuleCodeCache::lookup(const std::string& key, Monoton return CacheLookupResult{"", false, false}; } - return CacheLookupResult{entry.code, false, true}; + return CacheLookupResult{entry.module, false, true}; } -void DynamicModuleCodeCache::markInProgress(const std::string& key, MonotonicTime now) { +void DynamicModuleCache::markInProgress(const std::string& key, MonotonicTime now) { Thread::LockGuard guard(mutex_); now += time_offset_for_testing_; - CodeCacheEntry& entry = cache_[key]; + ModuleCacheEntry& entry = cache_[key]; entry.in_progress = true; entry.use_time = now; entry.fetch_time = now; } -void DynamicModuleCodeCache::update(const std::string& key, const std::string& code, - MonotonicTime now) { +void DynamicModuleCache::update(const std::string& key, const std::string& module, + MonotonicTime now) { Thread::LockGuard guard(mutex_); now += time_offset_for_testing_; - CodeCacheEntry& entry = cache_[key]; - entry.code = code; + ModuleCacheEntry& entry = cache_[key]; + entry.module = module; entry.in_progress = false; entry.use_time = now; entry.fetch_time = now; } -size_t DynamicModuleCodeCache::size() const { +size_t DynamicModuleCache::size() const { Thread::LockGuard guard(mutex_); return cache_.size(); } -void DynamicModuleCodeCache::clear() { +void DynamicModuleCache::clear() { Thread::LockGuard guard(mutex_); cache_.clear(); time_offset_for_testing_ = MonotonicTime::duration{}; } -void DynamicModuleCodeCache::setTimeOffsetForTesting(MonotonicTime::duration offset) { +void DynamicModuleCache::setTimeOffsetForTesting(MonotonicTime::duration offset) { Thread::LockGuard guard(mutex_); time_offset_for_testing_ = offset; } -void DynamicModuleCodeCache::removeExpiredEntries(MonotonicTime now) { +void DynamicModuleCache::removeExpiredEntries(MonotonicTime now) { // Called with mutex held. for (auto it = cache_.begin(); it != cache_.end();) { - const CodeCacheEntry& entry = it->second; + const ModuleCacheEntry& entry = it->second; // Don't remove in-progress entries. if (entry.in_progress) { @@ -130,7 +130,7 @@ void DynamicModuleCodeCache::removeExpiredEntries(MonotonicTime now) { auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); bool expired = false; - if (entry.code.empty()) { + if (entry.module.empty()) { // Negative cache entry. expired = elapsed >= NEGATIVE_CACHE_SECONDS; } else { diff --git a/source/extensions/dynamic_modules/code_cache.h b/source/extensions/dynamic_modules/module_cache.h similarity index 65% rename from source/extensions/dynamic_modules/code_cache.h rename to source/extensions/dynamic_modules/module_cache.h index 21f7ac80a039c..e04bca8de5dcd 100644 --- a/source/extensions/dynamic_modules/code_cache.h +++ b/source/extensions/dynamic_modules/module_cache.h @@ -5,16 +5,11 @@ #include #include "envoy/event/deferred_deletable.h" -#include "envoy/event/dispatcher.h" -#include "envoy/init/manager.h" -#include "envoy/stats/scope.h" -#include "envoy/upstream/cluster_manager.h" #include "source/common/common/lock_guard.h" #include "source/common/common/logger.h" #include "source/common/common/thread.h" #include "source/common/config/remote_data_fetcher.h" -#include "source/common/init/target_impl.h" #include "absl/container/flat_hash_map.h" @@ -23,10 +18,10 @@ namespace Extensions { namespace DynamicModules { /** - * Represents an entry in the code cache. + * Represents an entry in the module cache. */ -struct CodeCacheEntry { - std::string code; // Module binary data. +struct ModuleCacheEntry { + std::string module; // Module binary data. bool in_progress; // Fetch is ongoing. MonotonicTime use_time; // Last access time. MonotonicTime fetch_time; // When the module was fetched. @@ -36,8 +31,8 @@ struct CodeCacheEntry { * Result of a cache lookup operation. */ struct CacheLookupResult { - // The cached code if found and valid, empty string otherwise. - std::string code; + // The cached module if found and valid, empty string otherwise. + std::string module; // True if a fetch operation is already in progress for this key. bool fetch_in_progress; // True if a cache entry exists (even if in_progress or expired for negative cache). @@ -46,28 +41,23 @@ struct CacheLookupResult { /** * Callback invoked when async fetch completes. - * @param code The fetched module bytes, or empty string on failure. + * @param module The fetched module bytes, or empty string on failure. */ -using FetchCallback = std::function; +using FetchCallback = std::function; /** - * Thread-safe code cache for dynamic modules. - * - * Features: - * - Keyed by SHA256 hash of module content. - * - 24-hour TTL for cached entries. - * - 10-second negative caching for failed fetches. - * - In-progress tracking to avoid duplicate fetches. + * Thread-safe cache for remotely fetched dynamic module binaries, keyed by SHA256 hash. + * Supports positive caching (24h TTL), negative caching (10s TTL), and in-progress tracking. */ -class DynamicModuleCodeCache : public Logger::Loggable { +class DynamicModuleCache : public Logger::Loggable { public: // Cache TTL in seconds (24 hours). static constexpr int CACHE_TTL_SECONDS = 24 * 3600; // Negative cache TTL in seconds (10 seconds). static constexpr int NEGATIVE_CACHE_SECONDS = 10; - DynamicModuleCodeCache() = default; - ~DynamicModuleCodeCache() = default; + DynamicModuleCache() = default; + ~DynamicModuleCache() = default; /** * Looks up an entry in the cache. @@ -85,12 +75,12 @@ class DynamicModuleCodeCache : public Logger::Loggable { void markInProgress(const std::string& key, MonotonicTime now); /** - * Updates a cache entry with fetched code. + * Updates a cache entry with fetched module. * @param key The SHA256 hash key. - * @param code The fetched module bytes (empty string indicates fetch failure). + * @param module The fetched module bytes (empty string indicates fetch failure). * @param now Current monotonic time. */ - void update(const std::string& key, const std::string& code, MonotonicTime now); + void update(const std::string& key, const std::string& module, MonotonicTime now); /** * Returns the current number of entries in the cache. @@ -112,27 +102,27 @@ class DynamicModuleCodeCache : public Logger::Loggable { void removeExpiredEntries(MonotonicTime now); mutable Thread::MutexBasicLockable mutex_; - absl::flat_hash_map cache_; + absl::flat_hash_map cache_; MonotonicTime::duration time_offset_for_testing_{}; }; /** - * Singleton accessor for the global code cache. + * Singleton accessor for the global module cache. */ -DynamicModuleCodeCache& getCodeCache(); +DynamicModuleCache& getModuleCache(); /** - * Clears the code cache. Primarily for testing. + * Clears the module cache. Primarily for testing. */ -void clearCodeCacheForTesting(); +void clearModuleCacheForTesting(); /** - * Sets a time offset for the code cache. Primarily for testing. + * Sets a time offset for the module cache. Primarily for testing. */ -void setTimeOffsetForCodeCacheForTesting(MonotonicTime::duration d); +void setTimeOffsetForModuleCacheForTesting(MonotonicTime::duration d); /** - * Adapter for remote data fetching that integrates with the code cache. + * Adapter for remote data fetching that integrates with the module cache. */ class RemoteDataFetcherAdapter : public Config::DataFetcher::RemoteDataFetcherCallback, public Event::DeferredDeletable { diff --git a/source/extensions/filters/http/dynamic_modules/BUILD b/source/extensions/filters/http/dynamic_modules/BUILD index cea2a38c9cb0d..d7fa95f990f6e 100644 --- a/source/extensions/filters/http/dynamic_modules/BUILD +++ b/source/extensions/filters/http/dynamic_modules/BUILD @@ -39,13 +39,10 @@ envoy_cc_library( ":filter_config_lib", ":filter_lib", "//envoy/init:manager_interface", - "//source/common/buffer:buffer_lib", - "//source/common/common:hex_lib", "//source/common/config:datasource_lib", - "//source/common/crypto:utility_lib", "//source/extensions/common/wasm:remote_async_datasource_lib", - "//source/extensions/dynamic_modules:code_cache_lib", "//source/extensions/dynamic_modules:dynamic_modules_lib", + "//source/extensions/dynamic_modules:module_cache_lib", "//source/extensions/filters/http/common:factory_base_lib", "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", ], diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index b6c36e7663618..9499dc4f78131 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -1,11 +1,8 @@ #include "source/extensions/filters/http/dynamic_modules/factory.h" -#include "source/common/buffer/buffer_impl.h" -#include "source/common/common/hex.h" #include "source/common/config/datasource.h" -#include "source/common/crypto/utility.h" #include "source/common/runtime/runtime_features.h" -#include "source/extensions/dynamic_modules/code_cache.h" +#include "source/extensions/dynamic_modules/module_cache.h" #include "source/extensions/filters/http/dynamic_modules/filter.h" #include "source/extensions/filters/http/dynamic_modules/filter_config.h" @@ -15,7 +12,6 @@ namespace Configuration { namespace { -// Helper function to load a module from bytes and create the filter config. absl::StatusOr< Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr> createFilterConfigFromBytes(absl::string_view module_bytes, absl::string_view sha256_hash, @@ -50,7 +46,6 @@ createFilterConfigFromBytes(absl::string_view module_bytes, absl::string_view sh std::move(dynamic_module.value()), scope, context); } -// Helper to create the filter factory callback from a filter config. Http::FilterFactoryCb createFilterFactoryCallback( Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr filter_config) { @@ -79,7 +74,6 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa const auto& module_config = proto_config.dynamic_module_config(); - // Check if the new 'module' field is set. if (module_config.has_module()) { return createFilterFactoryFromAsyncDataSource(proto_config, context, scope, init_manager); } @@ -133,6 +127,10 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa return createFilterFactoryCallback(filter_config.value()); } +// Handles the AsyncDataSource-based module loading path (local files, inline bytes, and remote +// HTTP). For remote sources, modules are cached by SHA256 hash with two fetch modes: +// - NACK mode: reject the config immediately, fetch in the background, succeed on retry. +// - Warming mode: block server init until the fetch completes (or fails). absl::StatusOr DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, @@ -141,14 +139,12 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( const auto& module_config = proto_config.dynamic_module_config(); const auto& async_source = module_config.module(); - // Use configured metrics namespace or fall back to the default. const std::string metrics_namespace = module_config.metrics_namespace().empty() ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) : module_config.metrics_namespace(); if (async_source.has_local()) { - // Synchronous path: local file or inline bytes. auto data_or_error = Config::DataSource::read(async_source.local(), true, context.api()); if (!data_or_error.ok()) { return absl::InvalidArgumentError("Failed to read module data: " + @@ -160,7 +156,6 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return absl::InvalidArgumentError("Module data is empty"); } - // Compute SHA256 for the local data (no verification needed, just for temp file naming). auto filter_config = createFilterConfigFromBytes(module_bytes, "", proto_config, context, scope); if (!filter_config.ok()) { @@ -172,7 +167,6 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( } if (async_source.has_remote()) { - // Asynchronous path: remote HTTP fetch. const auto& remote_source = async_source.remote(); const std::string& sha256_hash = remote_source.sha256(); @@ -180,15 +174,13 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return absl::InvalidArgumentError("SHA256 hash is required for remote module sources"); } - // Check the code cache first. - auto& code_cache = Extensions::DynamicModules::getCodeCache(); + auto& module_cache = Extensions::DynamicModules::getModuleCache(); auto now = context.mainThreadDispatcher().timeSource().monotonicTime(); - auto cache_result = code_cache.lookup(sha256_hash, now); + auto cache_result = module_cache.lookup(sha256_hash, now); - if (cache_result.cache_hit && !cache_result.code.empty()) { - // Cache hit with valid code - load synchronously. - auto filter_config = - createFilterConfigFromBytes(cache_result.code, sha256_hash, proto_config, context, scope); + if (cache_result.cache_hit && !cache_result.module.empty()) { + auto filter_config = createFilterConfigFromBytes(cache_result.module, sha256_hash, + proto_config, context, scope); if (!filter_config.ok()) { return filter_config.status(); } @@ -197,31 +189,31 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return createFilterFactoryCallback(filter_config.value()); } - if (cache_result.cache_hit && cache_result.code.empty()) { - // Negative cache hit (recent fetch failure). + if (cache_result.fetch_in_progress) { if (module_config.nack_on_module_cache_miss()) { - return absl::UnavailableError( - "Module fetch recently failed (negative cache hit), NACK'ing configuration"); + return absl::UnavailableError("Module fetch in progress, NACK'ing configuration"); } - // For warming mode with negative cache, we still need to try fetching again. + // TODO(kanurag94): support waiting on in-progress fetches in warming mode. + return absl::UnavailableError("Module fetch in progress"); } - if (cache_result.fetch_in_progress) { - // Another fetch is already in progress. + if (cache_result.cache_hit && cache_result.module.empty()) { + // Negative cache hit -- a recent fetch failed. In NACK mode, reject immediately. + // In warming mode, fall through to re-fetch (the negative TTL will have expired + // by the time we get a new config push anyway). if (module_config.nack_on_module_cache_miss()) { - return absl::UnavailableError("Module fetch in progress, NACK'ing configuration"); + return absl::UnavailableError( + "Module fetch recently failed (negative cache hit), NACK'ing configuration"); } - // For warming mode, we'd need to wait - but this is complex to implement. - // For now, treat as unavailable. - return absl::UnavailableError("Module fetch in progress"); } - // Need to fetch the module. + // NACK mode: kick off a background fetch, then NACK this config update. The control + // plane will re-push the config, and the next attempt will find the module in cache. if (module_config.nack_on_module_cache_miss()) { - // NACK mode: Start background fetch and reject this config. - code_cache.markInProgress(sha256_hash, now); + module_cache.markInProgress(sha256_hash, now); - // Create a holder structure that keeps both adapter and fetcher alive together. + // FetchHolder bundles the adapter + fetcher so they share the same DeferredDeletable + // lifetime, preventing use-after-free on the callback. struct FetchHolder : public Event::DeferredDeletable { Extensions::DynamicModules::RemoteDataFetcherAdapter adapter; std::unique_ptr fetcher; @@ -231,112 +223,72 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( }; auto holder = std::make_unique([sha256_hash, &context](const std::string& data) { - auto& cache = Extensions::DynamicModules::getCodeCache(); + auto& cache = Extensions::DynamicModules::getModuleCache(); auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); - - if (!data.empty()) { - // Verify SHA256. - Buffer::OwnedImpl buffer(data); - auto& crypto_util = Common::Crypto::UtilitySingleton::get(); - const std::string computed_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); - if (computed_hash == sha256_hash) { - cache.update(sha256_hash, data, fetch_time); - return; - } - // Hash mismatch - treat as failure. - ENVOY_LOG_MISC(warn, "Dynamic module SHA256 mismatch: expected {}, got {}", sha256_hash, - computed_hash); - } - // Fetch failed or hash mismatch - update with empty data for negative caching. - cache.update(sha256_hash, "", fetch_time); + // RemoteDataFetcher already verifies the SHA256 hash before calling onSuccess, + // so non-empty data here is guaranteed to be valid. + cache.update(sha256_hash, data, fetch_time); }); holder->fetcher = std::make_unique( context.clusterManager(), remote_source.http_uri(), sha256_hash, holder->adapter); holder->fetcher->fetch(); - // Defer deletion of the holder to ensure it lives until the callback is invoked. context.mainThreadDispatcher().deferredDelete(std::move(holder)); return absl::UnavailableError( "Remote module not in cache, background fetch started, NACK'ing configuration"); } - // Warming mode: Use init manager to block until fetch completes. + // Warming mode: block server init until the fetch completes. The init manager will + // not transition to Initialized until the RemoteAsyncDataProvider signals ready(). if (init_manager == nullptr) { return absl::InvalidArgumentError( "Init manager required for warming mode with remote module sources"); } - // Mark as in progress. - code_cache.markInProgress(sha256_hash, now); + module_cache.markInProgress(sha256_hash, now); - // Create a shared state to hold the fetched data, filter config, and keep the provider alive. + // AsyncLoadState is shared between the fetch callback (which populates filter_config) + // and the returned factory callback (which reads it). Also prevents the + // RemoteAsyncDataProvider from being destroyed before the fetch completes. struct AsyncLoadState { - std::string module_bytes; Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr filter_config; RemoteAsyncDataProviderPtr remote_provider; - bool fetch_completed{false}; - bool fetch_success{false}; }; auto state = std::make_shared(); - // Use RemoteAsyncDataProvider for the fetch. - // The callback will be invoked when the fetch completes. + // SHA256 verification is handled by the underlying RemoteDataFetcher. + // Capture a weak_ptr to break the reference cycle: state owns remote_provider, + // and remote_provider's callback would otherwise prevent state from being freed. + std::weak_ptr weak_state = state; state->remote_provider = std::make_unique( context.clusterManager(), *init_manager, remote_source, context.mainThreadDispatcher(), context.api().randomGenerator(), false, - [state, sha256_hash, proto_config_copy = proto_config, &context, &scope, + [weak_state, sha256_hash, proto_config_copy = proto_config, &context, &scope, metrics_namespace](const std::string& data) { - auto& cache = Extensions::DynamicModules::getCodeCache(); + auto& cache = Extensions::DynamicModules::getModuleCache(); auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); + cache.update(sha256_hash, data, fetch_time); - state->fetch_completed = true; - if (!data.empty()) { - // Verify SHA256. - Buffer::OwnedImpl buffer(data); - auto& crypto_util = Common::Crypto::UtilitySingleton::get(); - const std::string computed_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); - if (computed_hash == sha256_hash) { - state->module_bytes = data; - state->fetch_success = true; - cache.update(sha256_hash, data, fetch_time); - - // Now create the filter config. - auto filter_config = - createFilterConfigFromBytes(data, sha256_hash, proto_config_copy, context, scope); - if (filter_config.ok()) { - state->filter_config = filter_config.value(); - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - } - return; - } - ENVOY_LOG_MISC(warn, "Dynamic module SHA256 mismatch: expected {}, got {}", sha256_hash, - computed_hash); + auto state = weak_state.lock(); + if (!state || data.empty()) { + return; + } + auto filter_config = + createFilterConfigFromBytes(data, sha256_hash, proto_config_copy, context, scope); + if (filter_config.ok()) { + state->filter_config = filter_config.value(); + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); } - cache.update(sha256_hash, "", fetch_time); }); - // Return a factory callback that uses the async-loaded config. + // If the fetch failed, filter_config will be null and we silently skip (fail-open). return [state](Http::FilterChainFactoryCallbacks& callbacks) -> void { if (!state->filter_config) { - // Module failed to load - skip adding filter (fail open behavior for warming mode). return; } - - const std::string& worker_name = callbacks.dispatcher().name(); - auto pos = worker_name.find_first_of('_'); - ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); - uint32_t worker_index; - if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { - IS_ENVOY_BUG("failed to parse worker index from name"); - } - auto filter = - std::make_shared( - state->filter_config, state->filter_config->stats_scope_->symbolTable(), - worker_index); - filter->initializeInModuleFilter(); - callbacks.addStreamFilter(filter); + createFilterFactoryCallback(state->filter_config)(callbacks); }; } diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index 18bfb524da33a..780b0ac901941 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.validate.h" diff --git a/test/extensions/dynamic_modules/BUILD b/test/extensions/dynamic_modules/BUILD index e5fec93a27484..0da118a50a4a2 100644 --- a/test/extensions/dynamic_modules/BUILD +++ b/test/extensions/dynamic_modules/BUILD @@ -71,10 +71,10 @@ envoy_cc_test( ) envoy_cc_test( - name = "code_cache_test", - srcs = ["code_cache_test.cc"], + name = "module_cache_test", + srcs = ["module_cache_test.cc"], deps = [ - "//source/extensions/dynamic_modules:code_cache_lib", + "//source/extensions/dynamic_modules:module_cache_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", ], diff --git a/test/extensions/dynamic_modules/code_cache_test.cc b/test/extensions/dynamic_modules/module_cache_test.cc similarity index 71% rename from test/extensions/dynamic_modules/code_cache_test.cc rename to test/extensions/dynamic_modules/module_cache_test.cc index 3db6be1a8e2e8..f10b2374e06c5 100644 --- a/test/extensions/dynamic_modules/code_cache_test.cc +++ b/test/extensions/dynamic_modules/module_cache_test.cc @@ -1,4 +1,4 @@ -#include "source/extensions/dynamic_modules/code_cache.h" +#include "source/extensions/dynamic_modules/module_cache.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" @@ -9,25 +9,25 @@ namespace Envoy { namespace Extensions { namespace DynamicModules { -class CodeCacheTest : public testing::Test { +class ModuleCacheTest : public testing::Test { protected: - void SetUp() override { clearCodeCacheForTesting(); } + void SetUp() override { clearModuleCacheForTesting(); } - void TearDown() override { clearCodeCacheForTesting(); } + void TearDown() override { clearModuleCacheForTesting(); } }; -TEST_F(CodeCacheTest, LookupMiss) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, LookupMiss) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); auto result = cache.lookup("nonexistent_key", now); EXPECT_FALSE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_TRUE(result.code.empty()); + EXPECT_TRUE(result.module.empty()); } -TEST_F(CodeCacheTest, MarkInProgressAndLookup) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, MarkInProgressAndLookup) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.markInProgress("test_key", now); @@ -36,11 +36,11 @@ TEST_F(CodeCacheTest, MarkInProgressAndLookup) { auto result = cache.lookup("test_key", now); EXPECT_TRUE(result.cache_hit); EXPECT_TRUE(result.fetch_in_progress); - EXPECT_TRUE(result.code.empty()); + EXPECT_TRUE(result.module.empty()); } -TEST_F(CodeCacheTest, UpdateWithCodeAndLookup) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, UpdateWithModuleAndLookup) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.markInProgress("test_key", now); @@ -49,32 +49,32 @@ TEST_F(CodeCacheTest, UpdateWithCodeAndLookup) { auto result = cache.lookup("test_key", now); EXPECT_TRUE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.code, "module_binary_data"); + EXPECT_EQ(result.module, "module_binary_data"); } -TEST_F(CodeCacheTest, NegativeCaching) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, NegativeCaching) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - // Update with empty code (failure). + // Update with empty module (failure). cache.update("test_key", "", now); // Lookup within negative cache TTL. auto result = cache.lookup("test_key", now); EXPECT_TRUE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_TRUE(result.code.empty()); + EXPECT_TRUE(result.module.empty()); // Lookup after negative cache TTL expires (10 seconds). MonotonicTime after_expiry = now + std::chrono::seconds(11); result = cache.lookup("test_key", after_expiry); EXPECT_FALSE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_TRUE(result.code.empty()); + EXPECT_TRUE(result.module.empty()); } -TEST_F(CodeCacheTest, PositiveCacheTTL) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, PositiveCacheTTL) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.update("test_key", "module_binary_data", now); @@ -83,17 +83,17 @@ TEST_F(CodeCacheTest, PositiveCacheTTL) { MonotonicTime within_ttl = now + std::chrono::hours(23); auto result = cache.lookup("test_key", within_ttl); EXPECT_TRUE(result.cache_hit); - EXPECT_EQ(result.code, "module_binary_data"); + EXPECT_EQ(result.module, "module_binary_data"); // Lookup after cache TTL expires. MonotonicTime after_ttl = now + std::chrono::hours(25); result = cache.lookup("test_key", after_ttl); EXPECT_FALSE(result.cache_hit); - EXPECT_TRUE(result.code.empty()); + EXPECT_TRUE(result.module.empty()); } -TEST_F(CodeCacheTest, MultipleEntries) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, MultipleEntries) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.update("key1", "data1", now); @@ -104,19 +104,19 @@ TEST_F(CodeCacheTest, MultipleEntries) { auto result1 = cache.lookup("key1", now); EXPECT_TRUE(result1.cache_hit); - EXPECT_EQ(result1.code, "data1"); + EXPECT_EQ(result1.module, "data1"); auto result2 = cache.lookup("key2", now); EXPECT_TRUE(result2.cache_hit); - EXPECT_EQ(result2.code, "data2"); + EXPECT_EQ(result2.module, "data2"); auto result3 = cache.lookup("key3", now); EXPECT_TRUE(result3.cache_hit); - EXPECT_EQ(result3.code, "data3"); + EXPECT_EQ(result3.module, "data3"); } -TEST_F(CodeCacheTest, Clear) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, Clear) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.update("key1", "data1", now); @@ -130,23 +130,23 @@ TEST_F(CodeCacheTest, Clear) { EXPECT_FALSE(result.cache_hit); } -TEST_F(CodeCacheTest, GlobalCacheAccessor) { +TEST_F(ModuleCacheTest, GlobalCacheAccessor) { MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - auto& cache1 = getCodeCache(); + auto& cache1 = getModuleCache(); cache1.update("global_key", "global_data", now); - auto& cache2 = getCodeCache(); + auto& cache2 = getModuleCache(); auto result = cache2.lookup("global_key", now); EXPECT_TRUE(result.cache_hit); - EXPECT_EQ(result.code, "global_data"); + EXPECT_EQ(result.module, "global_data"); // Both should be the same instance. EXPECT_EQ(&cache1, &cache2); } -TEST_F(CodeCacheTest, InProgressDoesNotExpire) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, InProgressDoesNotExpire) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.markInProgress("test_key", now); @@ -158,8 +158,8 @@ TEST_F(CodeCacheTest, InProgressDoesNotExpire) { EXPECT_TRUE(result.fetch_in_progress); } -TEST_F(CodeCacheTest, UpdateClearsInProgress) { - DynamicModuleCodeCache cache; +TEST_F(ModuleCacheTest, UpdateClearsInProgress) { + DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.markInProgress("test_key", now); @@ -171,7 +171,7 @@ TEST_F(CodeCacheTest, UpdateClearsInProgress) { result = cache.lookup("test_key", now); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.code, "module_data"); + EXPECT_EQ(result.module, "module_data"); } } // namespace DynamicModules diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD index 9fe79ff9aedfd..0e11041a1ce4c 100644 --- a/test/extensions/filters/http/dynamic_modules/BUILD +++ b/test/extensions/filters/http/dynamic_modules/BUILD @@ -21,7 +21,7 @@ envoy_cc_test( "//source/common/crypto:utility_lib", "//source/common/http:message_lib", "//source/common/stats:isolated_store_lib", - "//source/extensions/dynamic_modules:code_cache_lib", + "//source/extensions/dynamic_modules:module_cache_lib", "//source/extensions/filters/http/dynamic_modules:factory_lib", "//test/extensions/dynamic_modules:util", "//test/mocks/http:http_mocks", diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index c454d9273fce3..e7e0056d97f98 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -10,7 +10,7 @@ #include "source/common/crypto/utility.h" #include "source/common/http/message_impl.h" #include "source/common/stats/isolated_store_impl.h" -#include "source/extensions/dynamic_modules/code_cache.h" +#include "source/extensions/dynamic_modules/module_cache.h" #include "source/extensions/filters/http/dynamic_modules/factory.h" #include "test/extensions/dynamic_modules/util.h" @@ -44,17 +44,7 @@ class DynamicModuleFilterConfigTest : public Event::TestUsingSimulatedTime, publ .WillByDefault(ReturnRef(dispatcher_)); } - void SetUp() override { Extensions::DynamicModules::clearCodeCacheForTesting(); } - - void initializeForRemote() { - retry_timer_ = new Event::MockTimer(); - - EXPECT_CALL(dispatcher_, createTimer_(_)) - .WillOnce(testing::Invoke([this](Event::TimerCb timer_cb) { - retry_timer_cb_ = timer_cb; - return retry_timer_; - })); - } + void SetUp() override { Extensions::DynamicModules::clearModuleCacheForTesting(); } NiceMock listener_info_; Stats::IsolatedStoreImpl stats_store_; @@ -65,8 +55,6 @@ class DynamicModuleFilterConfigTest : public Event::TestUsingSimulatedTime, publ NiceMock cluster_manager_; Init::ExpectableWatcherImpl init_watcher_; NiceMock dispatcher_; - Event::MockTimer* retry_timer_; - Event::TimerCb retry_timer_cb_; NiceMock context_; }; @@ -156,10 +144,6 @@ TEST_F(DynamicModuleFilterConfigTest, InlineBytesLoading) { EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); } -// Remote loading tests are covered by RemoteLoadingNackOnCacheMiss which tests the -// fetch and cache mechanism. The warming mode path is complex due to stats lifecycle -// issues and is not tested here. Integration tests should cover the full flow. - TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackOnCacheMiss) { const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); @@ -233,10 +217,6 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackOnCacheMiss) { dispatcher_.clearDeferredDeleteList(); } -// Note: RemoteMissingSha256 test is not included because the proto validation -// already enforces that sha256 must be non-empty for remote sources. The validation -// happens during config parsing, not in our factory code. - TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { const std::string yaml = R"EOF( dynamic_module_config: @@ -273,6 +253,412 @@ TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to read module data")); } +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackFetchFailure) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + nack_on_module_cache_miss: true + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::Invoke( + [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callbacks.onFailure(request, Http::AsyncClient::FailureReason::Reset); + return &request; + })); + + DynamicModuleConfigFactory factory; + + // First attempt NACKs; the background fetch fails, creating a negative cache entry. + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Second attempt hits the negative cache, no new fetch. + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("negative cache hit")); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + EXPECT_EQ(init_manager2.state(), Init::Manager::State::Initialized); + + dispatcher_.clearDeferredDeleteList(); +} + +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackFetchInProgress) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + nack_on_module_cache_miss: true + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillRepeatedly(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + + Http::AsyncClient::Callbacks* async_callbacks = nullptr; + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::Invoke( + [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + async_callbacks = &callbacks; + return &request; + })); + + DynamicModuleConfigFactory factory; + + // NACK; the background fetch has started but hasn't completed yet. + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + + // Second attempt sees the in-progress fetch. + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("fetch in progress")); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + + // Now let the background fetch complete. + ASSERT_NE(async_callbacks, nullptr); + Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(module_bytes); + async_callbacks->onSuccess(request, std::move(response)); + + // Third attempt should find the module in cache. + Init::ManagerImpl init_manager3{"init_manager3"}; + Init::ExpectableWatcherImpl init_watcher3; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager3)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher3, ready()); + init_manager3.initialize(init_watcher3); + EXPECT_EQ(init_manager3.state(), Init::Manager::State::Initialized); + + dispatcher_.clearDeferredDeleteList(); +} + +// Exercises the full NACK-mode cache lifecycle: fail, negative-cache, expire, succeed, expire. +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackCacheLifecycle) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + nack_on_module_cache_miss: true + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillRepeatedly(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + + int send_count = 0; + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillRepeatedly(testing::Invoke( + [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + send_count++; + if (send_count == 1) { + callbacks.onFailure(request, Http::AsyncClient::FailureReason::Reset); + } else { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(module_bytes); + callbacks.onSuccess(request, std::move(response)); + } + return &request; + })); + + DynamicModuleConfigFactory factory; + + // First attempt: NACK, background fetch fails, lands in negative cache. + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + + // Negative cache hit. + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("negative cache hit")); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + + // Advance past the 10s negative cache TTL. + Extensions::DynamicModules::setTimeOffsetForModuleCacheForTesting(std::chrono::seconds(11)); + + // Negative cache expired, re-fetch succeeds but still NACKs (always NACK on first load). + Init::ManagerImpl init_manager3{"init_manager3"}; + Init::ExpectableWatcherImpl init_watcher3; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager3)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + + EXPECT_CALL(init_watcher3, ready()); + init_manager3.initialize(init_watcher3); + + // Cache hit, module loads successfully. + Init::ManagerImpl init_manager4{"init_manager4"}; + Init::ExpectableWatcherImpl init_watcher4; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager4)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher4, ready()); + init_manager4.initialize(init_watcher4); + EXPECT_EQ(init_manager4.state(), Init::Manager::State::Initialized); + + // Advance past the 24h positive cache TTL. + Extensions::DynamicModules::setTimeOffsetForModuleCacheForTesting( + std::chrono::seconds(11 + 24 * 3600 + 1)); + + // Positive cache expired, back to NACK. + Init::ManagerImpl init_manager5{"init_manager5"}; + Init::ExpectableWatcherImpl init_watcher5; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager5)); + + cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); + + EXPECT_CALL(init_watcher5, ready()); + init_manager5.initialize(init_watcher5); + + dispatcher_.clearDeferredDeleteList(); +} + +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::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().add(module_bytes); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + init_manager_.initialize(init_watcher_); + EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); +} + +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + // Set num_retries: 0 so RemoteAsyncDataProvider won't try to use the retry timer. + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256, R"EOF( + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::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"}}})); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + init_manager_.initialize(init_watcher_); + EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); + + // Fetch failed so the callback is a no-op (fail-open). + NiceMock filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(0); + cb_or_error.value()(filter_callback); +} + } // namespace Configuration } // namespace Server } // namespace Envoy From ec4159ffaa0ffd6babcc57bac1d3f27834eb82f2 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Thu, 5 Feb 2026 18:32:16 +0530 Subject: [PATCH 03/21] attempt fixing coverage Signed-off-by: Anurag Aggarwal --- .../dynamic_modules/v3/dynamic_modules.proto | 1 - test/coverage.yaml | 1 + .../filters/http/dynamic_modules/BUILD | 2 + .../http/dynamic_modules/config_test.cc | 50 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto index fd4981752d2a3..5ee9b95c59efa 100644 --- a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto +++ b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto @@ -5,7 +5,6 @@ package envoy.extensions.dynamic_modules.v3; import "envoy/config/core/v3/base.proto"; import "udpa/annotations/status.proto"; -import "validate/validate.proto"; option java_package = "io.envoyproxy.envoy.extensions.dynamic_modules.v3"; option java_outer_classname = "DynamicModulesProto"; diff --git a/test/coverage.yaml b/test/coverage.yaml index c6e9590fc464d..67bf15ac97d28 100644 --- a/test/coverage.yaml +++ b/test/coverage.yaml @@ -85,4 +85,5 @@ directories: source/extensions/health_checkers/grpc: 92.3 source/extensions/config_subscription/rest: 94.9 source/extensions/matching/input_matchers/cel_matcher: 100.0 + source/extensions/dynamic_modules: 92.0 # Filesystem error paths in newDynamicModuleFromBytes (file create/write/permissions/rename failures) are hard to test withot being flaky. ModuleCacheTest covers some of these error paths, but not all. source/extensions/dynamic_modules/sdk/cpp: 0.0 # SDK code self not directly tested diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD index 0e11041a1ce4c..dfd63275bf5cb 100644 --- a/test/extensions/filters/http/dynamic_modules/BUILD +++ b/test/extensions/filters/http/dynamic_modules/BUILD @@ -12,6 +12,7 @@ envoy_cc_test( name = "config_test", srcs = ["config_test.cc"], data = [ + "//test/extensions/dynamic_modules/test_data/c:http_filter_per_route_config_new_fail", "//test/extensions/dynamic_modules/test_data/c:no_op", ], deps = [ @@ -26,6 +27,7 @@ envoy_cc_test( "//test/extensions/dynamic_modules:util", "//test/mocks/http:http_mocks", "//test/mocks/network:network_mocks", + "//test/mocks/protobuf:protobuf_mocks", "//test/mocks/server:server_mocks", "//test/test_common:environment_lib", "//test/test_common:simulated_time_system_lib", diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index e7e0056d97f98..cc3dd90fc3669 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -16,6 +16,7 @@ #include "test/extensions/dynamic_modules/util.h" #include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" +#include "test/mocks/protobuf/mocks.h" #include "test/mocks/server/mocks.h" #include "test/test_common/environment.h" #include "test/test_common/utility.h" @@ -659,6 +660,55 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { cb_or_error.value()(filter_callback); } +TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigPerRouteConfigFail) { + // Set up the search path to find the test module. + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), + 1); + + // http_filter_per_route_config_new_fail exports the per-route config symbol but returns nullptr. + const std::string yaml = R"EOF( + dynamic_module_config: + name: "http_filter_per_route_config_new_fail" + do_not_close: true + per_route_config_name: "test" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilterPerRoute proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + NiceMock visitor; + auto config_or_error = + factory.createRouteSpecificFilterConfig(proto_config, context_.server_factory_context_, visitor); + EXPECT_FALSE(config_or_error.ok()); + EXPECT_THAT(config_or_error.status().message(), + testing::HasSubstr("Failed to create pre-route filter config")); + + TestEnvironment::unsetEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH"); +} + +TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigInvalidModule) { + const std::string yaml = R"EOF( + dynamic_module_config: + name: "nonexistent_module" + do_not_close: true + per_route_config_name: "test" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilterPerRoute proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + NiceMock visitor; + auto config_or_error = + factory.createRouteSpecificFilterConfig(proto_config, context_.server_factory_context_, visitor); + EXPECT_FALSE(config_or_error.ok()); + EXPECT_THAT(config_or_error.status().message(), + testing::HasSubstr("Failed to load dynamic module")); +} + } // namespace Configuration } // namespace Server } // namespace Envoy From 93caee8c3b6b78de4b7c4b7cb383c82f2496f83d Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Thu, 5 Feb 2026 19:19:46 +0530 Subject: [PATCH 04/21] attempt fixing coverage Signed-off-by: Anurag Aggarwal --- test/coverage.yaml | 4 +- .../http/dynamic_modules/config_test.cc | 173 +++++++++++++++++- 2 files changed, 171 insertions(+), 6 deletions(-) diff --git a/test/coverage.yaml b/test/coverage.yaml index 67bf15ac97d28..26b29ca065e2f 100644 --- a/test/coverage.yaml +++ b/test/coverage.yaml @@ -39,7 +39,7 @@ directories: source/extensions/filters/common/lua: 95.6 source/extensions/filters/http/cache: 95.9 source/extensions/filters/http/dynamic_forward_proxy: 94.8 - source/extensions/filters/http/dynamic_modules: 95.2 + source/extensions/filters/http/dynamic_modules: 95.0 source/extensions/filters/http/decompressor: 95.9 source/extensions/filters/http/ext_proc: 96.4 source/extensions/filters/http/grpc_json_reverse_transcoder: 94.8 @@ -85,5 +85,5 @@ directories: source/extensions/health_checkers/grpc: 92.3 source/extensions/config_subscription/rest: 94.9 source/extensions/matching/input_matchers/cel_matcher: 100.0 - source/extensions/dynamic_modules: 92.0 # Filesystem error paths in newDynamicModuleFromBytes (file create/write/permissions/rename failures) are hard to test withot being flaky. ModuleCacheTest covers some of these error paths, but not all. + source/extensions/dynamic_modules: 92.0 # Filesystem error paths in newDynamicModuleFromBytes are hard to test without being flaky source/extensions/dynamic_modules/sdk/cpp: 0.0 # SDK code self not directly tested diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index cc3dd90fc3669..1c0cdb1bec128 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -660,6 +660,171 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { cb_or_error.value()(filter_callback); } +TEST_F(DynamicModuleFilterConfigTest, EmptyLocalModuleData) { + const std::string empty_file = TestEnvironment::temporaryPath("empty_module.so"); + { std::ofstream f(empty_file); } + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + local: + filename: ")EOF", + empty_file, R"EOF(" + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Module data is empty")); +} + +TEST_F(DynamicModuleFilterConfigTest, ServerContextFactory) { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), + 1); + + const std::string yaml = R"EOF( + dynamic_module_config: + name: "no_op" + do_not_close: true + filter_name: "test_filter" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + EXPECT_FALSE( + factory.isTerminalFilterByProtoTyped(proto_config, context_.server_factory_context_)); + EXPECT_NO_THROW(factory.createFilterFactoryFromProtoWithServerContextTyped( + proto_config, "stats", context_.server_factory_context_)); + + TestEnvironment::unsetEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH"); +} + +TEST_F(DynamicModuleFilterConfigTest, ServerContextRemoteNoInitManager) { + const std::string yaml = R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: "abc123" + do_not_close: true + filter_name: "test_filter" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + EXPECT_THROW(factory.createFilterFactoryFromProtoWithServerContextTyped( + proto_config, "stats", context_.server_factory_context_), + EnvoyException); +} + +TEST_F(DynamicModuleFilterConfigTest, WarmingModeFetchInProgress) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); + + // First: NACK mode starts a background fetch that stays in-progress. + const std::string nack_yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + nack_on_module_cache_miss: true + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter nack_proto; + TestUtility::loadFromYaml(nack_yaml, nack_proto); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillRepeatedly(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + + Http::AsyncClient::Callbacks* async_callbacks = nullptr; + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::Invoke( + [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + async_callbacks = &callbacks; + return &request; + })); + + DynamicModuleConfigFactory factory; + + auto cb_or_error = factory.createFilterFactoryFromProto(nack_proto, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + + // Second: warming mode (nack_on_module_cache_miss defaults to false) sees in-progress fetch. + const std::string warming_yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256, R"EOF( + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter warming_proto; + TestUtility::loadFromYaml(warming_yaml, warming_proto); + + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + cb_or_error = factory.createFilterFactoryFromProto(warming_proto, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_EQ(cb_or_error.status().message(), "Module fetch in progress"); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + + // Complete the background fetch to clean up. + ASSERT_NE(async_callbacks, nullptr); + Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(module_bytes); + async_callbacks->onSuccess(request, std::move(response)); + + dispatcher_.clearDeferredDeleteList(); +} + TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigPerRouteConfigFail) { // Set up the search path to find the test module. TestEnvironment::setEnvVar( @@ -680,8 +845,8 @@ TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigPerRouteConfigFail) { DynamicModuleConfigFactory factory; NiceMock visitor; - auto config_or_error = - factory.createRouteSpecificFilterConfig(proto_config, context_.server_factory_context_, visitor); + auto config_or_error = factory.createRouteSpecificFilterConfig( + proto_config, context_.server_factory_context_, visitor); EXPECT_FALSE(config_or_error.ok()); EXPECT_THAT(config_or_error.status().message(), testing::HasSubstr("Failed to create pre-route filter config")); @@ -702,8 +867,8 @@ TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigInvalidModule) { DynamicModuleConfigFactory factory; NiceMock visitor; - auto config_or_error = - factory.createRouteSpecificFilterConfig(proto_config, context_.server_factory_context_, visitor); + auto config_or_error = factory.createRouteSpecificFilterConfig( + proto_config, context_.server_factory_context_, visitor); EXPECT_FALSE(config_or_error.ok()); EXPECT_THAT(config_or_error.status().message(), testing::HasSubstr("Failed to load dynamic module")); From 13d108908f17eb41fdc663d4c207a9960ff0c926 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Thu, 5 Feb 2026 19:53:10 +0530 Subject: [PATCH 05/21] lower coverage percentage for dynamic_modules filter Signed-off-by: Anurag Aggarwal --- test/coverage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/coverage.yaml b/test/coverage.yaml index 26b29ca065e2f..9857b48d19d03 100644 --- a/test/coverage.yaml +++ b/test/coverage.yaml @@ -39,7 +39,7 @@ directories: source/extensions/filters/common/lua: 95.6 source/extensions/filters/http/cache: 95.9 source/extensions/filters/http/dynamic_forward_proxy: 94.8 - source/extensions/filters/http/dynamic_modules: 95.0 + source/extensions/filters/http/dynamic_modules: 94.8 source/extensions/filters/http/decompressor: 95.9 source/extensions/filters/http/ext_proc: 96.4 source/extensions/filters/http/grpc_json_reverse_transcoder: 94.8 From b708a8f03d3a464ed9ed98b925a46b097faaccde Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Mon, 9 Feb 2026 13:23:53 +0530 Subject: [PATCH 06/21] fix issues related to fetch Signed-off-by: Anurag Aggarwal --- .../dynamic_modules/module_cache.cc | 29 ++++--- .../extensions/dynamic_modules/module_cache.h | 16 ++-- .../filters/http/dynamic_modules/factory.cc | 75 +++++++++++-------- .../dynamic_modules/module_cache_test.cc | 47 ++++++++---- .../http/dynamic_modules/config_test.cc | 6 ++ 5 files changed, 103 insertions(+), 70 deletions(-) diff --git a/source/extensions/dynamic_modules/module_cache.cc b/source/extensions/dynamic_modules/module_cache.cc index 814a466c02489..ea216df57487c 100644 --- a/source/extensions/dynamic_modules/module_cache.cc +++ b/source/extensions/dynamic_modules/module_cache.cc @@ -46,33 +46,33 @@ CacheLookupResult DynamicModuleCache::lookup(const std::string& key, MonotonicTi auto it = cache_.find(key); if (it == cache_.end()) { - return CacheLookupResult{"", false, false}; + return CacheLookupResult{nullptr, false, false}; } ModuleCacheEntry& entry = it->second; entry.use_time = now; if (entry.in_progress) { - return CacheLookupResult{"", true, true}; + return CacheLookupResult{nullptr, true, true}; } - // Check if this is a negative cache entry (empty module with recent fetch). - if (entry.module.empty()) { + // Check if this is a negative cache entry (no module data from a recent failed fetch). + if (!entry.module) { auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); if (elapsed < NEGATIVE_CACHE_SECONDS) { // Still within negative cache TTL - return empty but mark as cache hit. - return CacheLookupResult{"", false, true}; + return CacheLookupResult{nullptr, false, true}; } // Negative cache expired - treat as cache miss. cache_.erase(it); - return CacheLookupResult{"", false, false}; + return CacheLookupResult{nullptr, false, false}; } // Check TTL for positive cache entries. auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); if (elapsed >= CACHE_TTL_SECONDS) { cache_.erase(it); - return CacheLookupResult{"", false, false}; + return CacheLookupResult{nullptr, false, false}; } return CacheLookupResult{entry.module, false, true}; @@ -94,7 +94,7 @@ void DynamicModuleCache::update(const std::string& key, const std::string& modul now += time_offset_for_testing_; ModuleCacheEntry& entry = cache_[key]; - entry.module = module; + entry.module = module.empty() ? nullptr : std::make_shared(module); entry.in_progress = false; entry.use_time = now; entry.fetch_time = now; @@ -120,17 +120,14 @@ void DynamicModuleCache::removeExpiredEntries(MonotonicTime now) { // Called with mutex held. for (auto it = cache_.begin(); it != cache_.end();) { const ModuleCacheEntry& entry = it->second; - - // Don't remove in-progress entries. - if (entry.in_progress) { - ++it; - continue; - } - auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); bool expired = false; - if (entry.module.empty()) { + if (entry.in_progress) { + // Evict in-progress entries that have been stuck longer than the timeout. + // This prevents a hung fetch from permanently blocking a SHA256 key. + expired = elapsed >= IN_PROGRESS_TIMEOUT_SECONDS; + } else if (!entry.module) { // Negative cache entry. expired = elapsed >= NEGATIVE_CACHE_SECONDS; } else { diff --git a/source/extensions/dynamic_modules/module_cache.h b/source/extensions/dynamic_modules/module_cache.h index e04bca8de5dcd..a3cfaac099605 100644 --- a/source/extensions/dynamic_modules/module_cache.h +++ b/source/extensions/dynamic_modules/module_cache.h @@ -21,18 +21,19 @@ namespace DynamicModules { * Represents an entry in the module cache. */ struct ModuleCacheEntry { - std::string module; // Module binary data. - bool in_progress; // Fetch is ongoing. - MonotonicTime use_time; // Last access time. - MonotonicTime fetch_time; // When the module was fetched. + std::shared_ptr module; // Module binary data (nullptr for negative/in-progress). + bool in_progress{false}; // Fetch is ongoing. + MonotonicTime use_time; // Last access time. + MonotonicTime fetch_time; // When the module was fetched. }; /** * Result of a cache lookup operation. */ struct CacheLookupResult { - // The cached module if found and valid, empty string otherwise. - std::string module; + // The cached module if found and valid, nullptr otherwise. Shared ownership avoids copying + // potentially multi-MB module binaries on every cache hit. + std::shared_ptr module; // True if a fetch operation is already in progress for this key. bool fetch_in_progress; // True if a cache entry exists (even if in_progress or expired for negative cache). @@ -55,6 +56,9 @@ class DynamicModuleCache : public Logger::Loggable { static constexpr int CACHE_TTL_SECONDS = 24 * 3600; // Negative cache TTL in seconds (10 seconds). static constexpr int NEGATIVE_CACHE_SECONDS = 10; + // In-progress fetch timeout in seconds (5 minutes). Entries stuck in-progress longer than + // this are treated as failed and evicted, allowing a re-fetch on the next config push. + static constexpr int IN_PROGRESS_TIMEOUT_SECONDS = 5 * 60; DynamicModuleCache() = default; ~DynamicModuleCache() = default; diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 9499dc4f78131..9afc89cdf29a4 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -178,8 +178,8 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( auto now = context.mainThreadDispatcher().timeSource().monotonicTime(); auto cache_result = module_cache.lookup(sha256_hash, now); - if (cache_result.cache_hit && !cache_result.module.empty()) { - auto filter_config = createFilterConfigFromBytes(cache_result.module, sha256_hash, + if (cache_result.cache_hit && cache_result.module) { + auto filter_config = createFilterConfigFromBytes(*cache_result.module, sha256_hash, proto_config, context, scope); if (!filter_config.ok()) { return filter_config.status(); @@ -197,10 +197,9 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return absl::UnavailableError("Module fetch in progress"); } - if (cache_result.cache_hit && cache_result.module.empty()) { + if (cache_result.cache_hit && !cache_result.module) { // Negative cache hit -- a recent fetch failed. In NACK mode, reject immediately. - // In warming mode, fall through to re-fetch (the negative TTL will have expired - // by the time we get a new config push anyway). + // In warming mode, fall through to re-fetch since the module may now be available. if (module_config.nack_on_module_cache_miss()) { return absl::UnavailableError( "Module fetch recently failed (negative cache hit), NACK'ing configuration"); @@ -212,29 +211,34 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( if (module_config.nack_on_module_cache_miss()) { module_cache.markInProgress(sha256_hash, now); - // FetchHolder bundles the adapter + fetcher so they share the same DeferredDeletable - // lifetime, preventing use-after-free on the callback. - struct FetchHolder : public Event::DeferredDeletable { - Extensions::DynamicModules::RemoteDataFetcherAdapter adapter; - std::unique_ptr fetcher; - - FetchHolder(Extensions::DynamicModules::FetchCallback cb) : adapter(std::move(cb)) {} - ~FetchHolder() override = default; - }; - - auto holder = std::make_unique([sha256_hash, &context](const std::string& data) { - auto& cache = Extensions::DynamicModules::getModuleCache(); - auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); - // RemoteDataFetcher already verifies the SHA256 hash before calling onSuccess, - // so non-empty data here is guaranteed to be valid. - cache.update(sha256_hash, data, fetch_time); - }); - - holder->fetcher = std::make_unique( - context.clusterManager(), remote_source.http_uri(), sha256_hash, holder->adapter); - holder->fetcher->fetch(); - - context.mainThreadDispatcher().deferredDelete(std::move(holder)); + // Use shared_ptr> to keep the adapter+fetcher alive until + // the fetch callback fires. The shared_ptr is captured by the callback closure, forming + // a reference cycle that keeps everything alive. The cycle is broken inside the callback + // via holder->release() + deferredDelete. Without this, calling deferredDelete immediately + // after fetch() would destroy the fetcher at the end of the current event loop iteration, + // canceling the in-flight HTTP request before the response arrives. + auto holder = std::make_shared>(); + + auto adapter = std::make_unique( + [sha256_hash, &context, holder](const std::string& data) { + auto& cache = Extensions::DynamicModules::getModuleCache(); + auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); + // RemoteDataFetcher already verifies the SHA256 hash before calling onSuccess, + // so non-empty data here is guaranteed to be valid. + cache.update(sha256_hash, data, fetch_time); + // Break the reference cycle and schedule cleanup. + if (*holder) { + context.mainThreadDispatcher().deferredDelete( + Event::DeferredDeletablePtr{holder->release()}); + } + }); + + auto fetcher = std::make_unique( + context.clusterManager(), remote_source.http_uri(), sha256_hash, *adapter); + auto fetcher_ptr = fetcher.get(); + adapter->setFetcher(std::move(fetcher)); + *holder = std::move(adapter); + fetcher_ptr->fetch(); return absl::UnavailableError( "Remote module not in cache, background fetch started, NACK'ing configuration"); @@ -272,15 +276,22 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( cache.update(sha256_hash, data, fetch_time); auto state = weak_state.lock(); - if (!state || data.empty()) { + if (data.empty()) { + ENVOY_LOG_MISC(warn, "Remote dynamic module fetch failed for SHA256 {}", sha256_hash); + return; + } + if (!state) { return; } auto filter_config = createFilterConfigFromBytes(data, sha256_hash, proto_config_copy, context, scope); - if (filter_config.ok()) { - state->filter_config = filter_config.value(); - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + if (!filter_config.ok()) { + ENVOY_LOG_MISC(warn, "Remote dynamic module fetched but failed to load for SHA256 {}: {}", + sha256_hash, filter_config.status().message()); + return; } + state->filter_config = filter_config.value(); + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); }); // If the fetch failed, filter_config will be null and we silently skip (fail-open). diff --git a/test/extensions/dynamic_modules/module_cache_test.cc b/test/extensions/dynamic_modules/module_cache_test.cc index f10b2374e06c5..09f53c10194b5 100644 --- a/test/extensions/dynamic_modules/module_cache_test.cc +++ b/test/extensions/dynamic_modules/module_cache_test.cc @@ -23,7 +23,7 @@ TEST_F(ModuleCacheTest, LookupMiss) { auto result = cache.lookup("nonexistent_key", now); EXPECT_FALSE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_TRUE(result.module.empty()); + EXPECT_EQ(result.module, nullptr); } TEST_F(ModuleCacheTest, MarkInProgressAndLookup) { @@ -36,7 +36,7 @@ TEST_F(ModuleCacheTest, MarkInProgressAndLookup) { auto result = cache.lookup("test_key", now); EXPECT_TRUE(result.cache_hit); EXPECT_TRUE(result.fetch_in_progress); - EXPECT_TRUE(result.module.empty()); + EXPECT_EQ(result.module, nullptr); } TEST_F(ModuleCacheTest, UpdateWithModuleAndLookup) { @@ -49,7 +49,8 @@ TEST_F(ModuleCacheTest, UpdateWithModuleAndLookup) { auto result = cache.lookup("test_key", now); EXPECT_TRUE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.module, "module_binary_data"); + ASSERT_NE(result.module, nullptr); + EXPECT_EQ(*result.module, "module_binary_data"); } TEST_F(ModuleCacheTest, NegativeCaching) { @@ -63,14 +64,14 @@ TEST_F(ModuleCacheTest, NegativeCaching) { auto result = cache.lookup("test_key", now); EXPECT_TRUE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_TRUE(result.module.empty()); + EXPECT_EQ(result.module, nullptr); // Lookup after negative cache TTL expires (10 seconds). MonotonicTime after_expiry = now + std::chrono::seconds(11); result = cache.lookup("test_key", after_expiry); EXPECT_FALSE(result.cache_hit); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_TRUE(result.module.empty()); + EXPECT_EQ(result.module, nullptr); } TEST_F(ModuleCacheTest, PositiveCacheTTL) { @@ -83,13 +84,14 @@ TEST_F(ModuleCacheTest, PositiveCacheTTL) { MonotonicTime within_ttl = now + std::chrono::hours(23); auto result = cache.lookup("test_key", within_ttl); EXPECT_TRUE(result.cache_hit); - EXPECT_EQ(result.module, "module_binary_data"); + ASSERT_NE(result.module, nullptr); + EXPECT_EQ(*result.module, "module_binary_data"); // Lookup after cache TTL expires. MonotonicTime after_ttl = now + std::chrono::hours(25); result = cache.lookup("test_key", after_ttl); EXPECT_FALSE(result.cache_hit); - EXPECT_TRUE(result.module.empty()); + EXPECT_EQ(result.module, nullptr); } TEST_F(ModuleCacheTest, MultipleEntries) { @@ -104,15 +106,18 @@ TEST_F(ModuleCacheTest, MultipleEntries) { auto result1 = cache.lookup("key1", now); EXPECT_TRUE(result1.cache_hit); - EXPECT_EQ(result1.module, "data1"); + ASSERT_NE(result1.module, nullptr); + EXPECT_EQ(*result1.module, "data1"); auto result2 = cache.lookup("key2", now); EXPECT_TRUE(result2.cache_hit); - EXPECT_EQ(result2.module, "data2"); + ASSERT_NE(result2.module, nullptr); + EXPECT_EQ(*result2.module, "data2"); auto result3 = cache.lookup("key3", now); EXPECT_TRUE(result3.cache_hit); - EXPECT_EQ(result3.module, "data3"); + ASSERT_NE(result3.module, nullptr); + EXPECT_EQ(*result3.module, "data3"); } TEST_F(ModuleCacheTest, Clear) { @@ -139,23 +144,32 @@ TEST_F(ModuleCacheTest, GlobalCacheAccessor) { auto& cache2 = getModuleCache(); auto result = cache2.lookup("global_key", now); EXPECT_TRUE(result.cache_hit); - EXPECT_EQ(result.module, "global_data"); + ASSERT_NE(result.module, nullptr); + EXPECT_EQ(*result.module, "global_data"); // Both should be the same instance. EXPECT_EQ(&cache1, &cache2); } -TEST_F(ModuleCacheTest, InProgressDoesNotExpire) { +TEST_F(ModuleCacheTest, InProgressExpiresAfterTimeout) { DynamicModuleCache cache; MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); cache.markInProgress("test_key", now); - // Even after a long time, in-progress entries should not be removed. - MonotonicTime much_later = now + std::chrono::hours(100); - auto result = cache.lookup("test_key", much_later); + // Within the 5-minute timeout, in-progress entries should still be present. + MonotonicTime within_timeout = now + std::chrono::seconds(299); + auto result = cache.lookup("test_key", within_timeout); EXPECT_TRUE(result.cache_hit); EXPECT_TRUE(result.fetch_in_progress); + + // After the 5-minute timeout, in-progress entries should be evicted. + MonotonicTime after_timeout = + now + std::chrono::seconds(DynamicModuleCache::IN_PROGRESS_TIMEOUT_SECONDS + 1); + result = cache.lookup("test_key", after_timeout); + EXPECT_FALSE(result.cache_hit); + EXPECT_FALSE(result.fetch_in_progress); + EXPECT_EQ(result.module, nullptr); } TEST_F(ModuleCacheTest, UpdateClearsInProgress) { @@ -171,7 +185,8 @@ TEST_F(ModuleCacheTest, UpdateClearsInProgress) { result = cache.lookup("test_key", now); EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.module, "module_data"); + ASSERT_NE(result.module, nullptr); + EXPECT_EQ(*result.module, "module_data"); } } // namespace DynamicModules diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index 1c0cdb1bec128..0c17528bb53b5 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -395,6 +395,12 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackFetchInProgress) { EXPECT_CALL(init_watcher2, ready()); init_manager2.initialize(init_watcher2); + // Clearing the deferred delete list here simulates what the real event loop does at the end + // of each iteration. The shared_ptr> holder pattern must keep the adapter+fetcher + // alive through this; without it, the fetcher would be destroyed and async_callbacks would + // become a dangling pointer. + dispatcher_.clearDeferredDeleteList(); + // Now let the background fetch complete. ASSERT_NE(async_callbacks, nullptr); Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( From 52c7e773ce1a68f02cf7651d7ece27941154f57d Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Mon, 9 Feb 2026 17:23:11 +0530 Subject: [PATCH 07/21] fix lint Signed-off-by: Anurag Aggarwal --- source/extensions/dynamic_modules/module_cache.h | 9 +++++---- .../extensions/filters/http/dynamic_modules/factory.cc | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/source/extensions/dynamic_modules/module_cache.h b/source/extensions/dynamic_modules/module_cache.h index a3cfaac099605..a7b9b1315abbe 100644 --- a/source/extensions/dynamic_modules/module_cache.h +++ b/source/extensions/dynamic_modules/module_cache.h @@ -21,10 +21,11 @@ namespace DynamicModules { * Represents an entry in the module cache. */ struct ModuleCacheEntry { - std::shared_ptr module; // Module binary data (nullptr for negative/in-progress). - bool in_progress{false}; // Fetch is ongoing. - MonotonicTime use_time; // Last access time. - MonotonicTime fetch_time; // When the module was fetched. + std::shared_ptr + module; // Module binary data (nullptr for negative/in-progress). + bool in_progress{false}; // Fetch is ongoing. + MonotonicTime use_time; // Last access time. + MonotonicTime fetch_time; // When the module was fetched. }; /** diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 9afc89cdf29a4..5109bbde31d88 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -286,7 +286,8 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( auto filter_config = createFilterConfigFromBytes(data, sha256_hash, proto_config_copy, context, scope); if (!filter_config.ok()) { - ENVOY_LOG_MISC(warn, "Remote dynamic module fetched but failed to load for SHA256 {}: {}", + ENVOY_LOG_MISC(warn, + "Remote dynamic module fetched but failed to load for SHA256 {}: {}", sha256_hash, filter_config.status().message()); return; } From ec9edb52c7dcdddcc17cceaa3463909515e0ce25 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Mon, 9 Feb 2026 19:47:35 +0530 Subject: [PATCH 08/21] lower coverage due proto validation not invoking lines Signed-off-by: Anurag Aggarwal --- test/coverage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/coverage.yaml b/test/coverage.yaml index 5c174ddaaf03c..3de1a575e51d2 100644 --- a/test/coverage.yaml +++ b/test/coverage.yaml @@ -39,7 +39,7 @@ directories: source/extensions/filters/common/lua: 95.6 source/extensions/filters/http/cache: 95.9 source/extensions/filters/http/dynamic_forward_proxy: 94.8 - source/extensions/filters/http/dynamic_modules: 94.8 + source/extensions/filters/http/dynamic_modules: 94.5 source/extensions/filters/http/decompressor: 95.9 source/extensions/filters/http/ext_proc: 96.4 source/extensions/filters/http/grpc_json_reverse_transcoder: 94.8 From 5113c7e854a14392e87871b30a9e3391d8cff64d Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Wed, 11 Feb 2026 18:51:24 +0530 Subject: [PATCH 09/21] add changelog Signed-off-by: Anurag Aggarwal --- changelogs/current.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 48247565ee562..a1285611c47d0 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -233,6 +233,15 @@ new_features: functions by name via ``envoy_dynamic_module_callback_register_function`` and other modules can resolve them via ``envoy_dynamic_module_callback_get_function``, enabling zero-copy cross-module interactions analogous to ``dlsym``. +- area: dynamic_modules + change: | + Added support for loading dynamic module binaries from local files, inline bytes, or remote + URLs via the new :ref:`module + ` field in + ``DynamicModuleConfig``. For remote sources, SHA256 verification is required and fetched modules + are cached in-memory. The :ref:`nack_on_module_cache_miss + ` + field controls whether a remote cache miss NACKs the xDS update or blocks until the fetch completes. - area: formatter change: | Added ``QUERY_PARAMS`` support for substitution formatter to log all query params. From 028fb4d6d573beaef12a1fb4af8e2e1890266a2a Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 15:48:04 +0530 Subject: [PATCH 10/21] address review comments: remove cache related code Signed-off-by: Anurag Aggarwal --- .../dynamic_modules/v3/dynamic_modules.proto | 19 +- changelogs/current.yaml | 6 +- source/extensions/dynamic_modules/BUILD | 14 - .../dynamic_modules/module_cache.cc | 148 ------ .../extensions/dynamic_modules/module_cache.h | 153 ------ .../filters/http/dynamic_modules/BUILD | 1 - .../filters/http/dynamic_modules/factory.cc | 82 +--- test/extensions/dynamic_modules/BUILD | 10 - .../dynamic_modules/module_cache_test.cc | 194 -------- .../filters/http/dynamic_modules/BUILD | 1 - .../http/dynamic_modules/config_test.cc | 464 ------------------ 11 files changed, 6 insertions(+), 1086 deletions(-) delete mode 100644 source/extensions/dynamic_modules/module_cache.cc delete mode 100644 source/extensions/dynamic_modules/module_cache.h delete mode 100644 test/extensions/dynamic_modules/module_cache_test.cc diff --git a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto index cac6b0a3f408c..9aa3c70d6fe3a 100644 --- a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto +++ b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto @@ -31,7 +31,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // the ABI is stabilized, this restriction will be revisited. Until then, Envoy checks the hash of // the ABI header files to ensure that the dynamic modules are built against the same version of the // ABI. -// [#next-free-field: 8] +// [#next-free-field: 7] message DynamicModuleConfig { // The name of the dynamic module. // @@ -88,23 +88,8 @@ message DynamicModuleConfig { // The dynamic module binary to load. Supports local file paths (via ``local.filename``), // inline bytes (via ``local.inline_bytes``), or remote HTTP URLs (via ``remote``). // - // For remote sources, the ``sha256`` field is required and is used both for integrity - // verification and as the cache key. Fetched modules are cached in-memory for 24 hours; - // failed fetches are negatively cached for 10 seconds to avoid retry storms. + // For remote sources, the ``sha256`` field is required for integrity verification. // // When both ``name`` and ``module`` are set, ``module`` takes precedence. config.core.v3.AsyncDataSource module = 6; - - // Controls how a cache miss for a remote module is handled. - // - // When true (NACK mode), a cache miss causes an immediate NACK of the xDS config update. - // A background fetch is started and the module will be available on the next config push. - // - // When false (warming mode), the server blocks during initialization until the fetch - // completes or exhausts retries. - // - // Only applies to remote data sources via the ``module`` field. - // - // Defaults to ``false``. - bool nack_on_module_cache_miss = 7; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index c86459f46137d..2fff53c5c4879 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -463,10 +463,8 @@ new_features: Added support for loading dynamic module binaries from local files, inline bytes, or remote URLs via the new :ref:`module ` field in - ``DynamicModuleConfig``. For remote sources, SHA256 verification is required and fetched modules - are cached in-memory. The :ref:`nack_on_module_cache_miss - ` - field controls whether a remote cache miss NACKs the xDS update or blocks until the fetch completes. + ``DynamicModuleConfig``. For remote sources, SHA256 verification is required. The server + blocks during initialization until the remote fetch completes. - area: dynamic_modules change: | Network filter read and write buffers now persist after ``on_read``/``on_write`` callbacks, allowing diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index 3929b28113d25..f3f4137e3cd84 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -24,20 +24,6 @@ envoy_cc_library( ], ) -envoy_cc_library( - name = "module_cache_lib", - srcs = ["module_cache.cc"], - hdrs = [ - "module_cache.h", - ], - deps = [ - "//envoy/event:deferred_deletable", - "//source/common/common:logger_lib", - "//source/common/config:remote_data_fetcher_lib", - "@com_google_absl//absl/container:flat_hash_map", - ], -) - envoy_cc_library( name = "abi_impl", srcs = ["abi_impl.cc"], diff --git a/source/extensions/dynamic_modules/module_cache.cc b/source/extensions/dynamic_modules/module_cache.cc deleted file mode 100644 index ea216df57487c..0000000000000 --- a/source/extensions/dynamic_modules/module_cache.cc +++ /dev/null @@ -1,148 +0,0 @@ -#include "source/extensions/dynamic_modules/module_cache.h" - -#include "source/common/common/lock_guard.h" -#include "source/common/common/thread.h" - -namespace Envoy { -namespace Extensions { -namespace DynamicModules { - -namespace { -// Global module cache instance. -std::unique_ptr global_module_cache; -Thread::MutexBasicLockable global_module_cache_mutex; -} // namespace - -DynamicModuleCache& getModuleCache() { - Thread::LockGuard guard(global_module_cache_mutex); - if (!global_module_cache) { - global_module_cache = std::make_unique(); - } - return *global_module_cache; -} - -void clearModuleCacheForTesting() { - Thread::LockGuard guard(global_module_cache_mutex); - if (global_module_cache) { - global_module_cache->clear(); - } -} - -void setTimeOffsetForModuleCacheForTesting(MonotonicTime::duration d) { - Thread::LockGuard guard(global_module_cache_mutex); - if (global_module_cache) { - global_module_cache->setTimeOffsetForTesting(d); - } -} - -CacheLookupResult DynamicModuleCache::lookup(const std::string& key, MonotonicTime now) { - Thread::LockGuard guard(mutex_); - - // Apply time offset for testing. - now += time_offset_for_testing_; - - // Remove expired entries. - removeExpiredEntries(now); - - auto it = cache_.find(key); - if (it == cache_.end()) { - return CacheLookupResult{nullptr, false, false}; - } - - ModuleCacheEntry& entry = it->second; - entry.use_time = now; - - if (entry.in_progress) { - return CacheLookupResult{nullptr, true, true}; - } - - // Check if this is a negative cache entry (no module data from a recent failed fetch). - if (!entry.module) { - auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); - if (elapsed < NEGATIVE_CACHE_SECONDS) { - // Still within negative cache TTL - return empty but mark as cache hit. - return CacheLookupResult{nullptr, false, true}; - } - // Negative cache expired - treat as cache miss. - cache_.erase(it); - return CacheLookupResult{nullptr, false, false}; - } - - // Check TTL for positive cache entries. - auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); - if (elapsed >= CACHE_TTL_SECONDS) { - cache_.erase(it); - return CacheLookupResult{nullptr, false, false}; - } - - return CacheLookupResult{entry.module, false, true}; -} - -void DynamicModuleCache::markInProgress(const std::string& key, MonotonicTime now) { - Thread::LockGuard guard(mutex_); - now += time_offset_for_testing_; - - ModuleCacheEntry& entry = cache_[key]; - entry.in_progress = true; - entry.use_time = now; - entry.fetch_time = now; -} - -void DynamicModuleCache::update(const std::string& key, const std::string& module, - MonotonicTime now) { - Thread::LockGuard guard(mutex_); - now += time_offset_for_testing_; - - ModuleCacheEntry& entry = cache_[key]; - entry.module = module.empty() ? nullptr : std::make_shared(module); - entry.in_progress = false; - entry.use_time = now; - entry.fetch_time = now; -} - -size_t DynamicModuleCache::size() const { - Thread::LockGuard guard(mutex_); - return cache_.size(); -} - -void DynamicModuleCache::clear() { - Thread::LockGuard guard(mutex_); - cache_.clear(); - time_offset_for_testing_ = MonotonicTime::duration{}; -} - -void DynamicModuleCache::setTimeOffsetForTesting(MonotonicTime::duration offset) { - Thread::LockGuard guard(mutex_); - time_offset_for_testing_ = offset; -} - -void DynamicModuleCache::removeExpiredEntries(MonotonicTime now) { - // Called with mutex held. - for (auto it = cache_.begin(); it != cache_.end();) { - const ModuleCacheEntry& entry = it->second; - auto elapsed = std::chrono::duration_cast(now - entry.fetch_time).count(); - - bool expired = false; - if (entry.in_progress) { - // Evict in-progress entries that have been stuck longer than the timeout. - // This prevents a hung fetch from permanently blocking a SHA256 key. - expired = elapsed >= IN_PROGRESS_TIMEOUT_SECONDS; - } else if (!entry.module) { - // Negative cache entry. - expired = elapsed >= NEGATIVE_CACHE_SECONDS; - } else { - // Positive cache entry. - expired = elapsed >= CACHE_TTL_SECONDS; - } - - if (expired) { - cache_.erase(it++); - } else { - ++it; - } - } -} - -} // namespace DynamicModules -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/dynamic_modules/module_cache.h b/source/extensions/dynamic_modules/module_cache.h deleted file mode 100644 index a7b9b1315abbe..0000000000000 --- a/source/extensions/dynamic_modules/module_cache.h +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "envoy/event/deferred_deletable.h" - -#include "source/common/common/lock_guard.h" -#include "source/common/common/logger.h" -#include "source/common/common/thread.h" -#include "source/common/config/remote_data_fetcher.h" - -#include "absl/container/flat_hash_map.h" - -namespace Envoy { -namespace Extensions { -namespace DynamicModules { - -/** - * Represents an entry in the module cache. - */ -struct ModuleCacheEntry { - std::shared_ptr - module; // Module binary data (nullptr for negative/in-progress). - bool in_progress{false}; // Fetch is ongoing. - MonotonicTime use_time; // Last access time. - MonotonicTime fetch_time; // When the module was fetched. -}; - -/** - * Result of a cache lookup operation. - */ -struct CacheLookupResult { - // The cached module if found and valid, nullptr otherwise. Shared ownership avoids copying - // potentially multi-MB module binaries on every cache hit. - std::shared_ptr module; - // True if a fetch operation is already in progress for this key. - bool fetch_in_progress; - // True if a cache entry exists (even if in_progress or expired for negative cache). - bool cache_hit; -}; - -/** - * Callback invoked when async fetch completes. - * @param module The fetched module bytes, or empty string on failure. - */ -using FetchCallback = std::function; - -/** - * Thread-safe cache for remotely fetched dynamic module binaries, keyed by SHA256 hash. - * Supports positive caching (24h TTL), negative caching (10s TTL), and in-progress tracking. - */ -class DynamicModuleCache : public Logger::Loggable { -public: - // Cache TTL in seconds (24 hours). - static constexpr int CACHE_TTL_SECONDS = 24 * 3600; - // Negative cache TTL in seconds (10 seconds). - static constexpr int NEGATIVE_CACHE_SECONDS = 10; - // In-progress fetch timeout in seconds (5 minutes). Entries stuck in-progress longer than - // this are treated as failed and evicted, allowing a re-fetch on the next config push. - static constexpr int IN_PROGRESS_TIMEOUT_SECONDS = 5 * 60; - - DynamicModuleCache() = default; - ~DynamicModuleCache() = default; - - /** - * Looks up an entry in the cache. - * @param key The SHA256 hash key. - * @param now Current monotonic time for TTL checks. - * @return CacheLookupResult containing the lookup results. - */ - CacheLookupResult lookup(const std::string& key, MonotonicTime now); - - /** - * Marks a cache entry as in-progress for fetching. - * @param key The SHA256 hash key. - * @param now Current monotonic time. - */ - void markInProgress(const std::string& key, MonotonicTime now); - - /** - * Updates a cache entry with fetched module. - * @param key The SHA256 hash key. - * @param module The fetched module bytes (empty string indicates fetch failure). - * @param now Current monotonic time. - */ - void update(const std::string& key, const std::string& module, MonotonicTime now); - - /** - * Returns the current number of entries in the cache. - */ - size_t size() const; - - /** - * Clears the cache. Primarily for testing. - */ - void clear(); - - /** - * Sets a time offset for testing TTL behavior. - */ - void setTimeOffsetForTesting(MonotonicTime::duration offset); - -private: - // Removes expired entries during lookup. - void removeExpiredEntries(MonotonicTime now); - - mutable Thread::MutexBasicLockable mutex_; - absl::flat_hash_map cache_; - MonotonicTime::duration time_offset_for_testing_{}; -}; - -/** - * Singleton accessor for the global module cache. - */ -DynamicModuleCache& getModuleCache(); - -/** - * Clears the module cache. Primarily for testing. - */ -void clearModuleCacheForTesting(); - -/** - * Sets a time offset for the module cache. Primarily for testing. - */ -void setTimeOffsetForModuleCacheForTesting(MonotonicTime::duration d); - -/** - * Adapter for remote data fetching that integrates with the module cache. - */ -class RemoteDataFetcherAdapter : public Config::DataFetcher::RemoteDataFetcherCallback, - public Event::DeferredDeletable { -public: - explicit RemoteDataFetcherAdapter(FetchCallback cb) : callback_(std::move(cb)) {} - ~RemoteDataFetcherAdapter() override = default; - - // Config::DataFetcher::RemoteDataFetcherCallback - void onSuccess(const std::string& data) override { callback_(data); } - void onFailure(Config::DataFetcher::FailureReason) override { callback_(""); } - - void setFetcher(std::unique_ptr&& fetcher) { - fetcher_ = std::move(fetcher); - } - -private: - FetchCallback callback_; - std::unique_ptr fetcher_; -}; - -} // namespace DynamicModules -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/filters/http/dynamic_modules/BUILD b/source/extensions/filters/http/dynamic_modules/BUILD index b3ee218efc59b..5b542b7bd4ee4 100644 --- a/source/extensions/filters/http/dynamic_modules/BUILD +++ b/source/extensions/filters/http/dynamic_modules/BUILD @@ -42,7 +42,6 @@ envoy_cc_library( "//source/common/config:datasource_lib", "//source/extensions/common/wasm:remote_async_datasource_lib", "//source/extensions/dynamic_modules:dynamic_modules_lib", - "//source/extensions/dynamic_modules:module_cache_lib", "//source/extensions/filters/http/common:factory_base_lib", "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", ], diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 0f03e9f648908..1ada6b4beb203 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -2,7 +2,6 @@ #include "source/common/config/datasource.h" #include "source/common/runtime/runtime_features.h" -#include "source/extensions/dynamic_modules/module_cache.h" #include "source/extensions/filters/http/dynamic_modules/filter.h" #include "source/extensions/filters/http/dynamic_modules/filter_config.h" @@ -133,9 +132,8 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa } // Handles the AsyncDataSource-based module loading path (local files, inline bytes, and remote -// HTTP). For remote sources, modules are cached by SHA256 hash with two fetch modes: -// - NACK mode: reject the config immediately, fetch in the background, succeed on retry. -// - Warming mode: block server init until the fetch completes (or fails). +// HTTP). For remote sources, the server blocks during initialization (warming mode) until the +// fetch completes (or fails). absl::StatusOr DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, @@ -179,76 +177,6 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return absl::InvalidArgumentError("SHA256 hash is required for remote module sources"); } - auto& module_cache = Extensions::DynamicModules::getModuleCache(); - auto now = context.mainThreadDispatcher().timeSource().monotonicTime(); - auto cache_result = module_cache.lookup(sha256_hash, now); - - if (cache_result.cache_hit && cache_result.module) { - auto filter_config = createFilterConfigFromBytes(*cache_result.module, sha256_hash, - proto_config, context, scope); - if (!filter_config.ok()) { - return filter_config.status(); - } - - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - return createFilterFactoryCallback(filter_config.value()); - } - - if (cache_result.fetch_in_progress) { - if (module_config.nack_on_module_cache_miss()) { - return absl::UnavailableError("Module fetch in progress, NACK'ing configuration"); - } - // TODO(kanurag94): support waiting on in-progress fetches in warming mode. - return absl::UnavailableError("Module fetch in progress"); - } - - if (cache_result.cache_hit && !cache_result.module) { - // Negative cache hit -- a recent fetch failed. In NACK mode, reject immediately. - // In warming mode, fall through to re-fetch since the module may now be available. - if (module_config.nack_on_module_cache_miss()) { - return absl::UnavailableError( - "Module fetch recently failed (negative cache hit), NACK'ing configuration"); - } - } - - // NACK mode: kick off a background fetch, then NACK this config update. The control - // plane will re-push the config, and the next attempt will find the module in cache. - if (module_config.nack_on_module_cache_miss()) { - module_cache.markInProgress(sha256_hash, now); - - // Use shared_ptr> to keep the adapter+fetcher alive until - // the fetch callback fires. The shared_ptr is captured by the callback closure, forming - // a reference cycle that keeps everything alive. The cycle is broken inside the callback - // via holder->release() + deferredDelete. Without this, calling deferredDelete immediately - // after fetch() would destroy the fetcher at the end of the current event loop iteration, - // canceling the in-flight HTTP request before the response arrives. - auto holder = std::make_shared>(); - - auto adapter = std::make_unique( - [sha256_hash, &context, holder](const std::string& data) { - auto& cache = Extensions::DynamicModules::getModuleCache(); - auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); - // RemoteDataFetcher already verifies the SHA256 hash before calling onSuccess, - // so non-empty data here is guaranteed to be valid. - cache.update(sha256_hash, data, fetch_time); - // Break the reference cycle and schedule cleanup. - if (*holder) { - context.mainThreadDispatcher().deferredDelete( - Event::DeferredDeletablePtr{holder->release()}); - } - }); - - auto fetcher = std::make_unique( - context.clusterManager(), remote_source.http_uri(), sha256_hash, *adapter); - auto fetcher_ptr = fetcher.get(); - adapter->setFetcher(std::move(fetcher)); - *holder = std::move(adapter); - fetcher_ptr->fetch(); - - return absl::UnavailableError( - "Remote module not in cache, background fetch started, NACK'ing configuration"); - } - // Warming mode: block server init until the fetch completes. The init manager will // not transition to Initialized until the RemoteAsyncDataProvider signals ready(). if (init_manager == nullptr) { @@ -256,8 +184,6 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( "Init manager required for warming mode with remote module sources"); } - module_cache.markInProgress(sha256_hash, now); - // AsyncLoadState is shared between the fetch callback (which populates filter_config) // and the returned factory callback (which reads it). Also prevents the // RemoteAsyncDataProvider from being destroyed before the fetch completes. @@ -276,10 +202,6 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( context.api().randomGenerator(), false, [weak_state, sha256_hash, proto_config_copy = proto_config, &context, &scope, metrics_namespace](const std::string& data) { - auto& cache = Extensions::DynamicModules::getModuleCache(); - auto fetch_time = context.mainThreadDispatcher().timeSource().monotonicTime(); - cache.update(sha256_hash, data, fetch_time); - auto state = weak_state.lock(); if (data.empty()) { ENVOY_LOG_MISC(warn, "Remote dynamic module fetch failed for SHA256 {}", sha256_hash); diff --git a/test/extensions/dynamic_modules/BUILD b/test/extensions/dynamic_modules/BUILD index ae5dfd12adfec..64c32e4f9928f 100644 --- a/test/extensions/dynamic_modules/BUILD +++ b/test/extensions/dynamic_modules/BUILD @@ -55,16 +55,6 @@ envoy_cc_test( ], ) -envoy_cc_test( - name = "module_cache_test", - srcs = ["module_cache_test.cc"], - deps = [ - "//source/extensions/dynamic_modules:module_cache_lib", - "//test/test_common:simulated_time_system_lib", - "//test/test_common:utility_lib", - ], -) - envoy_cc_test_library( name = "util", srcs = ["util.cc"], diff --git a/test/extensions/dynamic_modules/module_cache_test.cc b/test/extensions/dynamic_modules/module_cache_test.cc deleted file mode 100644 index 09f53c10194b5..0000000000000 --- a/test/extensions/dynamic_modules/module_cache_test.cc +++ /dev/null @@ -1,194 +0,0 @@ -#include "source/extensions/dynamic_modules/module_cache.h" - -#include "test/test_common/simulated_time_system.h" -#include "test/test_common/utility.h" - -#include "gtest/gtest.h" - -namespace Envoy { -namespace Extensions { -namespace DynamicModules { - -class ModuleCacheTest : public testing::Test { -protected: - void SetUp() override { clearModuleCacheForTesting(); } - - void TearDown() override { clearModuleCacheForTesting(); } -}; - -TEST_F(ModuleCacheTest, LookupMiss) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - auto result = cache.lookup("nonexistent_key", now); - EXPECT_FALSE(result.cache_hit); - EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.module, nullptr); -} - -TEST_F(ModuleCacheTest, MarkInProgressAndLookup) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.markInProgress("test_key", now); - EXPECT_EQ(cache.size(), 1); - - auto result = cache.lookup("test_key", now); - EXPECT_TRUE(result.cache_hit); - EXPECT_TRUE(result.fetch_in_progress); - EXPECT_EQ(result.module, nullptr); -} - -TEST_F(ModuleCacheTest, UpdateWithModuleAndLookup) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.markInProgress("test_key", now); - cache.update("test_key", "module_binary_data", now); - - auto result = cache.lookup("test_key", now); - EXPECT_TRUE(result.cache_hit); - EXPECT_FALSE(result.fetch_in_progress); - ASSERT_NE(result.module, nullptr); - EXPECT_EQ(*result.module, "module_binary_data"); -} - -TEST_F(ModuleCacheTest, NegativeCaching) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - // Update with empty module (failure). - cache.update("test_key", "", now); - - // Lookup within negative cache TTL. - auto result = cache.lookup("test_key", now); - EXPECT_TRUE(result.cache_hit); - EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.module, nullptr); - - // Lookup after negative cache TTL expires (10 seconds). - MonotonicTime after_expiry = now + std::chrono::seconds(11); - result = cache.lookup("test_key", after_expiry); - EXPECT_FALSE(result.cache_hit); - EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.module, nullptr); -} - -TEST_F(ModuleCacheTest, PositiveCacheTTL) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.update("test_key", "module_binary_data", now); - - // Lookup within cache TTL (24 hours). - MonotonicTime within_ttl = now + std::chrono::hours(23); - auto result = cache.lookup("test_key", within_ttl); - EXPECT_TRUE(result.cache_hit); - ASSERT_NE(result.module, nullptr); - EXPECT_EQ(*result.module, "module_binary_data"); - - // Lookup after cache TTL expires. - MonotonicTime after_ttl = now + std::chrono::hours(25); - result = cache.lookup("test_key", after_ttl); - EXPECT_FALSE(result.cache_hit); - EXPECT_EQ(result.module, nullptr); -} - -TEST_F(ModuleCacheTest, MultipleEntries) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.update("key1", "data1", now); - cache.update("key2", "data2", now); - cache.update("key3", "data3", now); - - EXPECT_EQ(cache.size(), 3); - - auto result1 = cache.lookup("key1", now); - EXPECT_TRUE(result1.cache_hit); - ASSERT_NE(result1.module, nullptr); - EXPECT_EQ(*result1.module, "data1"); - - auto result2 = cache.lookup("key2", now); - EXPECT_TRUE(result2.cache_hit); - ASSERT_NE(result2.module, nullptr); - EXPECT_EQ(*result2.module, "data2"); - - auto result3 = cache.lookup("key3", now); - EXPECT_TRUE(result3.cache_hit); - ASSERT_NE(result3.module, nullptr); - EXPECT_EQ(*result3.module, "data3"); -} - -TEST_F(ModuleCacheTest, Clear) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.update("key1", "data1", now); - cache.update("key2", "data2", now); - EXPECT_EQ(cache.size(), 2); - - cache.clear(); - EXPECT_EQ(cache.size(), 0); - - auto result = cache.lookup("key1", now); - EXPECT_FALSE(result.cache_hit); -} - -TEST_F(ModuleCacheTest, GlobalCacheAccessor) { - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - auto& cache1 = getModuleCache(); - cache1.update("global_key", "global_data", now); - - auto& cache2 = getModuleCache(); - auto result = cache2.lookup("global_key", now); - EXPECT_TRUE(result.cache_hit); - ASSERT_NE(result.module, nullptr); - EXPECT_EQ(*result.module, "global_data"); - - // Both should be the same instance. - EXPECT_EQ(&cache1, &cache2); -} - -TEST_F(ModuleCacheTest, InProgressExpiresAfterTimeout) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.markInProgress("test_key", now); - - // Within the 5-minute timeout, in-progress entries should still be present. - MonotonicTime within_timeout = now + std::chrono::seconds(299); - auto result = cache.lookup("test_key", within_timeout); - EXPECT_TRUE(result.cache_hit); - EXPECT_TRUE(result.fetch_in_progress); - - // After the 5-minute timeout, in-progress entries should be evicted. - MonotonicTime after_timeout = - now + std::chrono::seconds(DynamicModuleCache::IN_PROGRESS_TIMEOUT_SECONDS + 1); - result = cache.lookup("test_key", after_timeout); - EXPECT_FALSE(result.cache_hit); - EXPECT_FALSE(result.fetch_in_progress); - EXPECT_EQ(result.module, nullptr); -} - -TEST_F(ModuleCacheTest, UpdateClearsInProgress) { - DynamicModuleCache cache; - MonotonicTime now = MonotonicTime(std::chrono::seconds(1000)); - - cache.markInProgress("test_key", now); - - auto result = cache.lookup("test_key", now); - EXPECT_TRUE(result.fetch_in_progress); - - cache.update("test_key", "module_data", now); - - result = cache.lookup("test_key", now); - EXPECT_FALSE(result.fetch_in_progress); - ASSERT_NE(result.module, nullptr); - EXPECT_EQ(*result.module, "module_data"); -} - -} // namespace DynamicModules -} // namespace Extensions -} // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD index dfd63275bf5cb..4b7b53f9f9a12 100644 --- a/test/extensions/filters/http/dynamic_modules/BUILD +++ b/test/extensions/filters/http/dynamic_modules/BUILD @@ -22,7 +22,6 @@ envoy_cc_test( "//source/common/crypto:utility_lib", "//source/common/http:message_lib", "//source/common/stats:isolated_store_lib", - "//source/extensions/dynamic_modules:module_cache_lib", "//source/extensions/filters/http/dynamic_modules:factory_lib", "//test/extensions/dynamic_modules:util", "//test/mocks/http:http_mocks", diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index 0c17528bb53b5..c1beb099a996c 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -1,4 +1,3 @@ -#include #include #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" @@ -10,7 +9,6 @@ #include "source/common/crypto/utility.h" #include "source/common/http/message_impl.h" #include "source/common/stats/isolated_store_impl.h" -#include "source/extensions/dynamic_modules/module_cache.h" #include "source/extensions/filters/http/dynamic_modules/factory.h" #include "test/extensions/dynamic_modules/util.h" @@ -45,8 +43,6 @@ class DynamicModuleFilterConfigTest : public Event::TestUsingSimulatedTime, publ .WillByDefault(ReturnRef(dispatcher_)); } - void SetUp() override { Extensions::DynamicModules::clearModuleCacheForTesting(); } - NiceMock listener_info_; Stats::IsolatedStoreImpl stats_store_; Stats::Scope& stats_scope_{*stats_store_.rootScope()}; @@ -145,79 +141,6 @@ TEST_F(DynamicModuleFilterConfigTest, InlineBytesLoading) { EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); } -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackOnCacheMiss) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256, R"EOF( - nack_on_module_cache_miss: true - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::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().add(module_bytes); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - DynamicModuleConfigFactory factory; - // First attempt should fail with NACK. - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); - - // Second attempt should succeed from cache. - Init::ManagerImpl init_manager2{"init_manager2"}; - Init::ExpectableWatcherImpl init_watcher2; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher2, ready()); - init_manager2.initialize(init_watcher2); - EXPECT_EQ(init_manager2.state(), Init::Manager::State::Initialized); - - dispatcher_.clearDeferredDeleteList(); -} - TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { const std::string yaml = R"EOF( dynamic_module_config: @@ -254,299 +177,6 @@ TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to read module data")); } -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackFetchFailure) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256, R"EOF( - nack_on_module_cache_miss: true - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::Invoke( - [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - callbacks.onFailure(request, Http::AsyncClient::FailureReason::Reset); - return &request; - })); - - DynamicModuleConfigFactory factory; - - // First attempt NACKs; the background fetch fails, creating a negative cache entry. - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); - - // Second attempt hits the negative cache, no new fetch. - Init::ManagerImpl init_manager2{"init_manager2"}; - Init::ExpectableWatcherImpl init_watcher2; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("negative cache hit")); - - EXPECT_CALL(init_watcher2, ready()); - init_manager2.initialize(init_watcher2); - EXPECT_EQ(init_manager2.state(), Init::Manager::State::Initialized); - - dispatcher_.clearDeferredDeleteList(); -} - -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackFetchInProgress) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256, R"EOF( - nack_on_module_cache_miss: true - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillRepeatedly(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - - Http::AsyncClient::Callbacks* async_callbacks = nullptr; - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::Invoke( - [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - async_callbacks = &callbacks; - return &request; - })); - - DynamicModuleConfigFactory factory; - - // NACK; the background fetch has started but hasn't completed yet. - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - - // Second attempt sees the in-progress fetch. - Init::ManagerImpl init_manager2{"init_manager2"}; - Init::ExpectableWatcherImpl init_watcher2; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("fetch in progress")); - - EXPECT_CALL(init_watcher2, ready()); - init_manager2.initialize(init_watcher2); - - // Clearing the deferred delete list here simulates what the real event loop does at the end - // of each iteration. The shared_ptr> holder pattern must keep the adapter+fetcher - // alive through this; without it, the fetcher would be destroyed and async_callbacks would - // become a dangling pointer. - dispatcher_.clearDeferredDeleteList(); - - // Now let the background fetch complete. - ASSERT_NE(async_callbacks, nullptr); - Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( - Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body().add(module_bytes); - async_callbacks->onSuccess(request, std::move(response)); - - // Third attempt should find the module in cache. - Init::ManagerImpl init_manager3{"init_manager3"}; - Init::ExpectableWatcherImpl init_watcher3; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager3)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher3, ready()); - init_manager3.initialize(init_watcher3); - EXPECT_EQ(init_manager3.state(), Init::Manager::State::Initialized); - - dispatcher_.clearDeferredDeleteList(); -} - -// Exercises the full NACK-mode cache lifecycle: fail, negative-cache, expire, succeed, expire. -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingNackCacheLifecycle) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256, R"EOF( - nack_on_module_cache_miss: true - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillRepeatedly(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - - int send_count = 0; - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillRepeatedly(testing::Invoke( - [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - send_count++; - if (send_count == 1) { - callbacks.onFailure(request, Http::AsyncClient::FailureReason::Reset); - } else { - Http::ResponseMessagePtr response( - new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ - new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body().add(module_bytes); - callbacks.onSuccess(request, std::move(response)); - } - return &request; - })); - - DynamicModuleConfigFactory factory; - - // First attempt: NACK, background fetch fails, lands in negative cache. - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - - // Negative cache hit. - Init::ManagerImpl init_manager2{"init_manager2"}; - Init::ExpectableWatcherImpl init_watcher2; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("negative cache hit")); - - EXPECT_CALL(init_watcher2, ready()); - init_manager2.initialize(init_watcher2); - - // Advance past the 10s negative cache TTL. - Extensions::DynamicModules::setTimeOffsetForModuleCacheForTesting(std::chrono::seconds(11)); - - // Negative cache expired, re-fetch succeeds but still NACKs (always NACK on first load). - Init::ManagerImpl init_manager3{"init_manager3"}; - Init::ExpectableWatcherImpl init_watcher3; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager3)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - - EXPECT_CALL(init_watcher3, ready()); - init_manager3.initialize(init_watcher3); - - // Cache hit, module loads successfully. - Init::ManagerImpl init_manager4{"init_manager4"}; - Init::ExpectableWatcherImpl init_watcher4; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager4)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher4, ready()); - init_manager4.initialize(init_watcher4); - EXPECT_EQ(init_manager4.state(), Init::Manager::State::Initialized); - - // Advance past the 24h positive cache TTL. - Extensions::DynamicModules::setTimeOffsetForModuleCacheForTesting( - std::chrono::seconds(11 + 24 * 3600 + 1)); - - // Positive cache expired, back to NACK. - Init::ManagerImpl init_manager5{"init_manager5"}; - Init::ExpectableWatcherImpl init_watcher5; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager5)); - - cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().code(), absl::StatusCode::kUnavailable); - - EXPECT_CALL(init_watcher5, ready()); - init_manager5.initialize(init_watcher5); - - dispatcher_.clearDeferredDeleteList(); -} - TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); @@ -737,100 +367,6 @@ TEST_F(DynamicModuleFilterConfigTest, ServerContextRemoteNoInitManager) { EnvoyException); } -TEST_F(DynamicModuleFilterConfigTest, WarmingModeFetchInProgress) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - // First: NACK mode starts a background fetch that stays in-progress. - const std::string nack_yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256, R"EOF( - nack_on_module_cache_miss: true - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter nack_proto; - TestUtility::loadFromYaml(nack_yaml, nack_proto); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillRepeatedly(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - - Http::AsyncClient::Callbacks* async_callbacks = nullptr; - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::Invoke( - [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - async_callbacks = &callbacks; - return &request; - })); - - DynamicModuleConfigFactory factory; - - auto cb_or_error = factory.createFilterFactoryFromProto(nack_proto, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - - // Second: warming mode (nack_on_module_cache_miss defaults to false) sees in-progress fetch. - const std::string warming_yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: )EOF", - sha256, R"EOF( - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter warming_proto; - TestUtility::loadFromYaml(warming_yaml, warming_proto); - - Init::ManagerImpl init_manager2{"init_manager2"}; - Init::ExpectableWatcherImpl init_watcher2; - EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); - - cb_or_error = factory.createFilterFactoryFromProto(warming_proto, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_EQ(cb_or_error.status().message(), "Module fetch in progress"); - - EXPECT_CALL(init_watcher2, ready()); - init_manager2.initialize(init_watcher2); - - // Complete the background fetch to clean up. - ASSERT_NE(async_callbacks, nullptr); - Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( - Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); - response->body().add(module_bytes); - async_callbacks->onSuccess(request, std::move(response)); - - dispatcher_.clearDeferredDeleteList(); -} - TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigPerRouteConfigFail) { // Set up the search path to find the test module. TestEnvironment::setEnvVar( From ce47d788fd94245284d9cb16868cad0127e5e561 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 17:02:05 +0530 Subject: [PATCH 11/21] refactoring: logging and avoid buffer copy Signed-off-by: Anurag Aggarwal --- source/extensions/dynamic_modules/BUILD | 3 +- .../dynamic_modules/dynamic_modules.cc | 11 ++- .../filters/http/dynamic_modules/factory.cc | 14 ++- .../filters/http/dynamic_modules/factory.h | 11 +-- .../http/dynamic_modules/config_test.cc | 87 +++++++++++++++++++ 5 files changed, 110 insertions(+), 16 deletions(-) diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index f3f4137e3cd84..78c0c01ab33d0 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -17,10 +17,9 @@ envoy_cc_library( ], deps = [ "//envoy/common:exception_lib", - "//source/common/buffer:buffer_lib", "//source/common/common:hex_lib", - "//source/common/crypto:utility_lib", "//source/extensions/dynamic_modules/abi", + "@boringssl//:ssl", ], ) diff --git a/source/extensions/dynamic_modules/dynamic_modules.cc b/source/extensions/dynamic_modules/dynamic_modules.cc index 844cc5b38e653..e5b48bb9d26fe 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.cc +++ b/source/extensions/dynamic_modules/dynamic_modules.cc @@ -7,12 +7,11 @@ #include "envoy/common/exception.h" -#include "source/common/buffer/buffer_impl.h" #include "source/common/common/hex.h" -#include "source/common/crypto/utility.h" #include "source/extensions/dynamic_modules/abi/abi.h" #include "absl/strings/str_cat.h" +#include "openssl/sha.h" namespace Envoy { namespace Extensions { @@ -159,10 +158,10 @@ absl::StatusOr newDynamicModuleFromBytes(absl::string_view mod return absl::InvalidArgumentError("Module bytes cannot be empty"); } - // Compute SHA256 hash of the module bytes. - auto& crypto_util = Common::Crypto::UtilitySingleton::get(); - Buffer::OwnedImpl buffer{std::string{module_bytes}}; - const std::string computed_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); + // Compute SHA256 hash of the module bytes directly without copying into a Buffer. + uint8_t digest[SHA256_DIGEST_LENGTH]; + SHA256(reinterpret_cast(module_bytes.data()), module_bytes.size(), digest); + const std::string computed_hash = Hex::encode(digest, SHA256_DIGEST_LENGTH); // Verify SHA256 hash if provided. if (!sha256_hash.empty() && computed_hash != sha256_hash) { diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 1ada6b4beb203..8dbb1789f670d 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -165,7 +165,10 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return filter_config.status(); } - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + } return createFilterFactoryCallback(filter_config.value()); } @@ -219,12 +222,17 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( return; } state->filter_config = filter_config.value(); - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + } }); - // If the fetch failed, filter_config will be null and we silently skip (fail-open). + // If the fetch failed, filter_config will be null and we skip (fail-open). return [state](Http::FilterChainFactoryCallbacks& callbacks) -> void { if (!state->filter_config) { + ENVOY_LOG_MISC(warn, + "Dynamic module filter skipped: remote module was not loaded (fail-open)"); return; } createFilterFactoryCallback(state->filter_config)(callbacks); diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index 780b0ac901941..e7ed806558bf7 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -39,11 +39,6 @@ class DynamicModuleConfigFactory Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, Init::Manager* init_manager = nullptr); - absl::StatusOr - createFilterFactoryFromAsyncDataSource(const FilterConfig& proto_config, - Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope, Init::Manager* init_manager); - absl::StatusOr createRouteSpecificFilterConfigTyped(const RouteConfigProto&, Server::Configuration::ServerFactoryContext&, @@ -55,6 +50,12 @@ class DynamicModuleConfigFactory Server::Configuration::ServerFactoryContext&) override { return proto_config.terminal_filter(); } + +private: + absl::StatusOr + createFilterFactoryFromAsyncDataSource(const FilterConfig& proto_config, + Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope, Init::Manager* init_manager); }; using UpstreamDynamicModuleConfigFactory = DynamicModuleConfigFactory; diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index c1beb099a996c..5ace732d5a474 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -296,6 +296,93 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { cb_or_error.value()(filter_callback); } +TEST_F(DynamicModuleFilterConfigTest, RemoteWithEmptySHA256) { + const std::string yaml = R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + do_not_close: true + filter_name: "test_filter" + )EOF"; + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("SHA256 hash is required")); +} + +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + // Use an incorrect SHA256 hash that won't match the actual module bytes. + const std::string wrong_sha256 = + "0000000000000000000000000000000000000000000000000000000000000000"; + + // Set num_retries: 0 so RemoteAsyncDataProvider won't try to use the retry timer. + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + wrong_sha256, R"EOF( + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::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().add(module_bytes); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + init_manager_.initialize(init_watcher_); + EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); + + // RemoteDataFetcher rejects the SHA256 mismatch, so filter_config stays null (fail-open). + NiceMock filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(0); + cb_or_error.value()(filter_callback); +} + TEST_F(DynamicModuleFilterConfigTest, EmptyLocalModuleData) { const std::string empty_file = TestEnvironment::temporaryPath("empty_module.so"); { std::ofstream f(empty_file); } From 0b63daa004332ffbdbf79f4ba95304a2786ce2fe Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 18:19:33 +0530 Subject: [PATCH 12/21] review: remove inline source Signed-off-by: Anurag Aggarwal --- .../dynamic_modules/v3/dynamic_modules.proto | 4 +-- changelogs/current.yaml | 2 +- .../filters/http/dynamic_modules/factory.cc | 12 +++++++-- .../filters/http/dynamic_modules/BUILD | 1 - .../http/dynamic_modules/config_test.cc | 27 +++++-------------- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto index 9aa3c70d6fe3a..f0ac00028d142 100644 --- a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto +++ b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto @@ -85,8 +85,8 @@ message DynamicModuleConfig { // Defaults to ``dynamicmodulescustom``. string metrics_namespace = 5; - // The dynamic module binary to load. Supports local file paths (via ``local.filename``), - // inline bytes (via ``local.inline_bytes``), or remote HTTP URLs (via ``remote``). + // The dynamic module binary to load. Currently supports local file paths + // (via ``local.filename``) and remote HTTP URLs (via ``remote``). // // For remote sources, the ``sha256`` field is required for integrity verification. // diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 2fff53c5c4879..75666b14b0373 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -460,7 +460,7 @@ new_features: cross-module interactions analogous to ``dlsym``. - area: dynamic_modules change: | - Added support for loading dynamic module binaries from local files, inline bytes, or remote + Added support for loading dynamic module binaries from local file paths or remote URLs via the new :ref:`module ` field in ``DynamicModuleConfig``. For remote sources, SHA256 verification is required. The server diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 8dbb1789f670d..2f496f44890a3 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -131,8 +131,8 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa return createFilterFactoryCallback(filter_config.value()); } -// Handles the AsyncDataSource-based module loading path (local files, inline bytes, and remote -// HTTP). For remote sources, the server blocks during initialization (warming mode) until the +// Handles the AsyncDataSource-based module loading path (local files and remote HTTP). +// For remote sources, the server blocks during initialization (warming mode) until the // fetch completes (or fails). absl::StatusOr DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( @@ -148,6 +148,14 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( : module_config.metrics_namespace(); if (async_source.has_local()) { + // Only local.filename is supported. Inline bytes/strings are not a good practice + // for binary module data. + if (!async_source.local().has_filename()) { + return absl::InvalidArgumentError( + "Only local.filename is supported for module sources; " + "inline_bytes and inline_string are not supported"); + } + auto data_or_error = Config::DataSource::read(async_source.local(), true, context.api()); if (!data_or_error.ok()) { return absl::InvalidArgumentError("Failed to read module data: " + diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD index 4b7b53f9f9a12..fbd929a6faa55 100644 --- a/test/extensions/filters/http/dynamic_modules/BUILD +++ b/test/extensions/filters/http/dynamic_modules/BUILD @@ -17,7 +17,6 @@ envoy_cc_test( ], deps = [ "//source/common/buffer:buffer_lib", - "//source/common/common:base64_lib", "//source/common/common:hex_lib", "//source/common/crypto:utility_lib", "//source/common/http:message_lib", diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index 5ace732d5a474..87162f1b2a3cc 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -4,7 +4,6 @@ #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.validate.h" #include "source/common/buffer/buffer_impl.h" -#include "source/common/common/base64.h" #include "source/common/common/hex.h" #include "source/common/crypto/utility.h" #include "source/common/http/message_impl.h" @@ -109,36 +108,24 @@ TEST_F(DynamicModuleFilterConfigTest, LocalFileLoading) { EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); } -TEST_F(DynamicModuleFilterConfigTest, InlineBytesLoading) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string yaml = absl::StrCat(R"EOF( +TEST_F(DynamicModuleFilterConfigTest, InlineBytesRejected) { + const std::string yaml = R"EOF( dynamic_module_config: module: local: - inline_bytes: ")EOF", - Base64::encode(module_bytes.data(), module_bytes.size()), - R"EOF(" + inline_bytes: "AAAA" do_not_close: true filter_name: "test_filter" - )EOF"); + )EOF"; envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; TestUtility::loadFromYaml(yaml, proto_config); DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), + testing::HasSubstr("Only local.filename is supported")); } TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { From b7936795e180d6ff15f14d448d9ecf5621c99438 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 18:52:03 +0530 Subject: [PATCH 13/21] refactor the code Signed-off-by: Anurag Aggarwal --- .../filters/http/dynamic_modules/BUILD | 1 - .../filters/http/dynamic_modules/factory.cc | 103 +++++++--------- .../dynamic_modules/dynamic_modules_test.cc | 28 +++++ .../http/dynamic_modules/config_test.cc | 116 ++++++++++++++---- 4 files changed, 167 insertions(+), 81 deletions(-) diff --git a/source/extensions/filters/http/dynamic_modules/BUILD b/source/extensions/filters/http/dynamic_modules/BUILD index 5b542b7bd4ee4..584b1a7407387 100644 --- a/source/extensions/filters/http/dynamic_modules/BUILD +++ b/source/extensions/filters/http/dynamic_modules/BUILD @@ -39,7 +39,6 @@ envoy_cc_library( ":filter_config_lib", ":filter_lib", "//envoy/init:manager_interface", - "//source/common/config:datasource_lib", "//source/extensions/common/wasm:remote_async_datasource_lib", "//source/extensions/dynamic_modules:dynamic_modules_lib", "//source/extensions/filters/http/common:factory_base_lib", diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 2f496f44890a3..e1986014f78e7 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -1,6 +1,5 @@ #include "source/extensions/filters/http/dynamic_modules/factory.h" -#include "source/common/config/datasource.h" #include "source/common/runtime/runtime_features.h" #include "source/extensions/filters/http/dynamic_modules/filter.h" #include "source/extensions/filters/http/dynamic_modules/filter_config.h" @@ -69,6 +68,39 @@ Http::FilterFactoryCb createFilterFactoryCallback( }; } +// Creates a filter factory callback from an already-loaded dynamic module. Shared by the +// legacy name-based path and the local file path. +absl::StatusOr +createFilterFactoryFromModule(Extensions::DynamicModules::DynamicModulePtr dynamic_module, + const FilterConfig& proto_config, const std::string& metrics_namespace, + Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope) { + std::string config; + if (proto_config.has_filter_config()) { + auto config_or_error = MessageUtil::anyToBytes(proto_config.filter_config()); + if (!config_or_error.ok()) { + return config_or_error.status(); + } + config = std::move(config_or_error.value()); + } + + auto filter_config = + Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( + proto_config.filter_name(), config, metrics_namespace, proto_config.terminal_filter(), + std::move(dynamic_module), scope, context); + if (!filter_config.ok()) { + return absl::InvalidArgumentError("Failed to create filter config: " + + std::string(filter_config.status().message())); + } + + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + } + + return createFilterFactoryCallback(filter_config.value()); +} + } // namespace absl::StatusOr DynamicModuleConfigFactory::createFilterFactory( @@ -95,40 +127,13 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa std::string(dynamic_module.status().message())); } - std::string config; - if (proto_config.has_filter_config()) { - auto config_or_error = MessageUtil::anyToBytes(proto_config.filter_config()); - RETURN_IF_NOT_OK_REF(config_or_error.status()); - config = std::move(config_or_error.value()); - } - - // Use configured metrics namespace or fall back to the default. const std::string metrics_namespace = module_config.metrics_namespace().empty() ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) : module_config.metrics_namespace(); - absl::StatusOr< - Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr> - filter_config = - Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - proto_config.filter_name(), config, metrics_namespace, proto_config.terminal_filter(), - std::move(dynamic_module.value()), scope, context); - - if (!filter_config.ok()) { - return absl::InvalidArgumentError("Failed to create filter config: " + - std::string(filter_config.status().message())); - } - - // When the runtime guard is enabled, register the metrics namespace as a custom stat namespace. - // This causes the namespace prefix to be stripped from prometheus output and no envoy_ prefix - // is added. This is the legacy behavior for backward compatibility. - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - } - - return createFilterFactoryCallback(filter_config.value()); + return createFilterFactoryFromModule(std::move(dynamic_module.value()), proto_config, + metrics_namespace, context, scope); } // Handles the AsyncDataSource-based module loading path (local files and remote HTTP). @@ -148,36 +153,22 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( : module_config.metrics_namespace(); if (async_source.has_local()) { - // Only local.filename is supported. Inline bytes/strings are not a good practice - // for binary module data. + // Only local.filename is supported for module sources. if (!async_source.local().has_filename()) { - return absl::InvalidArgumentError( - "Only local.filename is supported for module sources; " - "inline_bytes and inline_string are not supported"); - } - - auto data_or_error = Config::DataSource::read(async_source.local(), true, context.api()); - if (!data_or_error.ok()) { - return absl::InvalidArgumentError("Failed to read module data: " + - std::string(data_or_error.status().message())); - } - - const std::string& module_bytes = data_or_error.value(); - if (module_bytes.empty()) { - return absl::InvalidArgumentError("Module data is empty"); + return absl::InvalidArgumentError("Only local.filename is supported for module sources; " + "inline_bytes and inline_string are not supported"); } - auto filter_config = - createFilterConfigFromBytes(module_bytes, "", proto_config, context, scope); - if (!filter_config.ok()) { - return filter_config.status(); + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + async_source.local().filename(), module_config.do_not_close(), + module_config.load_globally()); + if (!dynamic_module.ok()) { + return absl::InvalidArgumentError("Failed to load dynamic module: " + + std::string(dynamic_module.status().message())); } - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - } - return createFilterFactoryCallback(filter_config.value()); + return createFilterFactoryFromModule(std::move(dynamic_module.value()), proto_config, + metrics_namespace, context, scope); } if (async_source.has_remote()) { @@ -213,11 +204,11 @@ DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( context.api().randomGenerator(), false, [weak_state, sha256_hash, proto_config_copy = proto_config, &context, &scope, metrics_namespace](const std::string& data) { - auto state = weak_state.lock(); if (data.empty()) { ENVOY_LOG_MISC(warn, "Remote dynamic module fetch failed for SHA256 {}", sha256_hash); return; } + auto state = weak_state.lock(); if (!state) { return; } diff --git a/test/extensions/dynamic_modules/dynamic_modules_test.cc b/test/extensions/dynamic_modules/dynamic_modules_test.cc index fc7673d73bec2..ac28b50b79974 100644 --- a/test/extensions/dynamic_modules/dynamic_modules_test.cc +++ b/test/extensions/dynamic_modules/dynamic_modules_test.cc @@ -288,6 +288,34 @@ TEST(CreateDynamicModulesFromBytes, InvalidModuleBytes) { EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); } +// Verify the temp file is created at expected path with restrictive permissions. +TEST(CreateDynamicModulesFromBytes, TempFilePermissions) { + std::string module_path = testSharedObjectPath("no_op", "c"); + std::ifstream file(module_path, std::ios::binary); + ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; + std::string module_bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + ASSERT_FALSE(module_bytes.empty()); + + auto& crypto_util = Common::Crypto::UtilitySingleton::get(); + Buffer::OwnedImpl buffer(module_bytes); + std::string expected_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); + + absl::StatusOr module = + newDynamicModuleFromBytes(module_bytes, expected_hash, false, false); + EXPECT_TRUE(module.ok()) << module.status().message(); + + const std::filesystem::path temp_file = + std::filesystem::temp_directory_path() / + fmt::format("envoy_dynmod_{}.so", expected_hash); + EXPECT_TRUE(std::filesystem::exists(temp_file)); + + // Verify 0600 permissions (no group/other access). + auto perms = std::filesystem::status(temp_file).permissions(); + EXPECT_EQ(perms & std::filesystem::perms::group_all, std::filesystem::perms::none); + EXPECT_EQ(perms & std::filesystem::perms::others_all, std::filesystem::perms::none); +} + } // namespace DynamicModules } // namespace Extensions } // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index 87162f1b2a3cc..2d2cc636b4846 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -161,7 +161,7 @@ TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); EXPECT_FALSE(cb_or_error.ok()); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to read module data")); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to load dynamic module")); } TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { @@ -218,6 +218,14 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { EXPECT_CALL(init_watcher_, ready()); init_manager_.initialize(init_watcher_); EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); + + // Exercise the returned factory callback to verify the filter is actually installed. + NiceMock filter_callback; + const std::string worker_name = "worker_0"; + NiceMock worker_dispatcher(worker_name); + ON_CALL(filter_callback, dispatcher()).WillByDefault(ReturnRef(worker_dispatcher)); + EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(1); + cb_or_error.value()(filter_callback); } TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { @@ -370,29 +378,6 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { cb_or_error.value()(filter_callback); } -TEST_F(DynamicModuleFilterConfigTest, EmptyLocalModuleData) { - const std::string empty_file = TestEnvironment::temporaryPath("empty_module.so"); - { std::ofstream f(empty_file); } - - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - local: - filename: ")EOF", - empty_file, R"EOF(" - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Module data is empty")); -} - TEST_F(DynamicModuleFilterConfigTest, ServerContextFactory) { TestEnvironment::setEnvVar( "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", @@ -490,6 +475,89 @@ TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigInvalidModule) { testing::HasSubstr("Failed to load dynamic module")); } +// Verify that a successful fetch with invalid (non-.so) module bytes fails gracefully. +TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingInvalidModuleBytes) { + const std::string invalid_bytes = "this is not a valid shared object binary"; + + const std::string sha256 = Hex::encode( + Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(invalid_bytes))); + + const std::string yaml = absl::StrCat(R"EOF( + dynamic_module_config: + module: + remote: + http_uri: + uri: https://example.com/module.so + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256, R"EOF( + do_not_close: true + filter_name: "test_filter" + )EOF"); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + NiceMock client; + NiceMock request(&client); + + cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); + EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) + .WillOnce(testing::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().add(invalid_bytes); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + DynamicModuleConfigFactory factory; + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); + + EXPECT_CALL(init_watcher_, ready()); + init_manager_.initialize(init_watcher_); + EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); + + // Fetch succeeded but dlopen fails on invalid bytes, so filter_config stays null. + NiceMock filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(0); + cb_or_error.value()(filter_callback); +} + +// Verify that when both name and module are set, module takes precedence. +TEST_F(DynamicModuleFilterConfigTest, ModulePrecedenceOverName) { + const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); + + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + dynamic_module_config: + name: "nonexistent_module_should_be_ignored" + module: + local: + filename: ")EOF", + module_path, R"EOF(" + do_not_close: true + filter_name: "test_filter" + )EOF")); + + envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + DynamicModuleConfigFactory factory; + // If name were used, this would fail because "nonexistent_module_should_be_ignored" doesn't exist. + // Since module takes precedence, it should succeed with the local file. + auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); +} + } // namespace Configuration } // namespace Server } // namespace Envoy From 29170e1cf9559c3028116224aeaecb6705cd64c9 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 18:57:09 +0530 Subject: [PATCH 14/21] fix some failing tests Signed-off-by: Anurag Aggarwal --- .../http/dynamic_modules/config_test.cc | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index 2d2cc636b4846..e0b584da0aa1f 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -291,28 +291,6 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { cb_or_error.value()(filter_callback); } -TEST_F(DynamicModuleFilterConfigTest, RemoteWithEmptySHA256) { - const std::string yaml = R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - do_not_close: true - filter_name: "test_filter" - )EOF"; - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("SHA256 hash is required")); -} - TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); @@ -322,12 +300,9 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { std::istreambuf_iterator()); ASSERT_FALSE(module_bytes.empty()); - // Use an incorrect SHA256 hash that won't match the actual module bytes. - const std::string wrong_sha256 = - "0000000000000000000000000000000000000000000000000000000000000000"; - // Set num_retries: 0 so RemoteAsyncDataProvider won't try to use the retry timer. - const std::string yaml = absl::StrCat(R"EOF( + // Use an incorrect SHA256 hash that won't match the actual module bytes. + const std::string yaml = R"EOF( dynamic_module_config: module: remote: @@ -337,11 +312,10 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { timeout: 5s retry_policy: num_retries: 0 - sha256: )EOF", - wrong_sha256, R"EOF( + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" do_not_close: true filter_name: "test_filter" - )EOF"); + )EOF"; envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; TestUtility::loadFromYaml(yaml, proto_config); From c186ec40d3a4a83bd46a6dcacdee4817d65418c1 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 19:24:29 +0530 Subject: [PATCH 15/21] simpler refactoring Signed-off-by: Anurag Aggarwal --- .../filters/http/dynamic_modules/factory.cc | 328 ++++++++---------- .../filters/http/dynamic_modules/factory.h | 6 +- .../dynamic_modules/dynamic_modules_test.cc | 3 +- .../http/dynamic_modules/config_test.cc | 12 +- 4 files changed, 157 insertions(+), 192 deletions(-) diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index e1986014f78e7..eca0959d7b0e8 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -8,46 +8,72 @@ namespace Envoy { namespace Server { namespace Configuration { -namespace { +absl::StatusOr DynamicModuleConfigFactory::createFilterFactory( + const FilterConfig& proto_config, const std::string&, + Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, + Init::Manager* init_manager) { -absl::StatusOr< - Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr> -createFilterConfigFromBytes(absl::string_view module_bytes, absl::string_view sha256_hash, - const FilterConfig& proto_config, - Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope) { const auto& module_config = proto_config.dynamic_module_config(); - auto dynamic_module = Extensions::DynamicModules::newDynamicModuleFromBytes( - module_bytes, sha256_hash, module_config.do_not_close(), module_config.load_globally()); + // Remote source requires async warming — handle separately. + if (module_config.has_module() && module_config.module().has_remote()) { + return createFilterFactoryFromRemoteSource(proto_config, context, scope, init_manager); + } + + // Load the module: either from a local file path or by name. + absl::StatusOr dynamic_module; + if (module_config.has_module()) { + if (!module_config.module().has_local() || !module_config.module().local().has_filename()) { + return absl::InvalidArgumentError("Only local.filename is supported for module sources; " + "inline_bytes and inline_string are not supported"); + } + dynamic_module = Extensions::DynamicModules::newDynamicModule( + module_config.module().local().filename(), module_config.do_not_close(), + module_config.load_globally()); + } else { + if (module_config.name().empty()) { + return absl::InvalidArgumentError( + "Either 'name' or 'module' must be specified in dynamic_module_config"); + } + dynamic_module = Extensions::DynamicModules::newDynamicModuleByName( + module_config.name(), module_config.do_not_close(), module_config.load_globally()); + } if (!dynamic_module.ok()) { - return absl::InvalidArgumentError("Failed to load dynamic module from bytes: " + + return absl::InvalidArgumentError("Failed to load dynamic module: " + std::string(dynamic_module.status().message())); } std::string config; if (proto_config.has_filter_config()) { auto config_or_error = MessageUtil::anyToBytes(proto_config.filter_config()); - if (!config_or_error.ok()) { - return config_or_error.status(); - } + RETURN_IF_NOT_OK_REF(config_or_error.status()); config = std::move(config_or_error.value()); } + // Use configured metrics namespace or fall back to the default. const std::string metrics_namespace = module_config.metrics_namespace().empty() ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) : module_config.metrics_namespace(); - return Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - proto_config.filter_name(), config, metrics_namespace, proto_config.terminal_filter(), - std::move(dynamic_module.value()), scope, context); -} + absl::StatusOr< + Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr> + filter_config = + Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( + proto_config.filter_name(), config, metrics_namespace, proto_config.terminal_filter(), + std::move(dynamic_module.value()), scope, context); + + if (!filter_config.ok()) { + return absl::InvalidArgumentError("Failed to create filter config: " + + std::string(filter_config.status().message())); + } + + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + } -Http::FilterFactoryCb createFilterFactoryCallback( - Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr - filter_config) { - return [config = std::move(filter_config)](Http::FilterChainFactoryCallbacks& callbacks) -> void { + return [config = filter_config.value()](Http::FilterChainFactoryCallbacks& callbacks) -> void { const std::string& worker_name = callbacks.dispatcher().name(); auto pos = worker_name.find_first_of('_'); ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); @@ -59,72 +85,30 @@ Http::FilterFactoryCb createFilterFactoryCallback( std::make_shared( config, config->stats_scope_->symbolTable(), worker_index); callbacks.addStreamFilter(filter); - - // The addStreamFilter() will call the setDecoderFilterCallbacks first then - // setEncoderFilterCallbacks. - // We can initialize the in-module filter after we have both callbacks to ensure the in module - // filter can access all the necessary information during creation. + // addStreamFilter() sets decoder/encoder filter callbacks. Initialize the in-module filter + // after so it can access all necessary context during creation. filter->initializeInModuleFilter(); }; } -// Creates a filter factory callback from an already-loaded dynamic module. Shared by the -// legacy name-based path and the local file path. absl::StatusOr -createFilterFactoryFromModule(Extensions::DynamicModules::DynamicModulePtr dynamic_module, - const FilterConfig& proto_config, const std::string& metrics_namespace, - Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope) { - std::string config; - if (proto_config.has_filter_config()) { - auto config_or_error = MessageUtil::anyToBytes(proto_config.filter_config()); - if (!config_or_error.ok()) { - return config_or_error.status(); - } - config = std::move(config_or_error.value()); - } - - auto filter_config = - Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - proto_config.filter_name(), config, metrics_namespace, proto_config.terminal_filter(), - std::move(dynamic_module), scope, context); - if (!filter_config.ok()) { - return absl::InvalidArgumentError("Failed to create filter config: " + - std::string(filter_config.status().message())); - } - - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - } - - return createFilterFactoryCallback(filter_config.value()); -} - -} // namespace - -absl::StatusOr DynamicModuleConfigFactory::createFilterFactory( - const FilterConfig& proto_config, const std::string&, - Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, - Init::Manager* init_manager) { +DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( + const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope, Init::Manager* init_manager) { const auto& module_config = proto_config.dynamic_module_config(); + const auto& remote_source = module_config.module().remote(); + const std::string& sha256_hash = remote_source.sha256(); - if (module_config.has_module()) { - return createFilterFactoryFromAsyncDataSource(proto_config, context, scope, init_manager); + if (sha256_hash.empty()) { + return absl::InvalidArgumentError("SHA256 hash is required for remote module sources"); } - // Legacy path: load module by name. - if (module_config.name().empty()) { + if (init_manager == nullptr) { return absl::InvalidArgumentError( - "Either 'name' or 'module' must be specified in dynamic_module_config"); - } - - auto dynamic_module = Extensions::DynamicModules::newDynamicModuleByName( - module_config.name(), module_config.do_not_close(), module_config.load_globally()); - if (!dynamic_module.ok()) { - return absl::InvalidArgumentError("Failed to load dynamic module: " + - std::string(dynamic_module.status().message())); + "Remote module sources require an init manager for warming and are not supported via " + "the server context factory path (createFilterFactoryFromProtoWithServerContext). " + "Use the listener-level filter config instead"); } const std::string metrics_namespace = @@ -132,113 +116,95 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) : module_config.metrics_namespace(); - return createFilterFactoryFromModule(std::move(dynamic_module.value()), proto_config, - metrics_namespace, context, scope); -} - -// Handles the AsyncDataSource-based module loading path (local files and remote HTTP). -// For remote sources, the server blocks during initialization (warming mode) until the -// fetch completes (or fails). -absl::StatusOr -DynamicModuleConfigFactory::createFilterFactoryFromAsyncDataSource( - const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope, Init::Manager* init_manager) { - - const auto& module_config = proto_config.dynamic_module_config(); - const auto& async_source = module_config.module(); - - const std::string metrics_namespace = - module_config.metrics_namespace().empty() - ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) - : module_config.metrics_namespace(); - - if (async_source.has_local()) { - // Only local.filename is supported for module sources. - if (!async_source.local().has_filename()) { - return absl::InvalidArgumentError("Only local.filename is supported for module sources; " - "inline_bytes and inline_string are not supported"); - } - - auto dynamic_module = Extensions::DynamicModules::newDynamicModule( - async_source.local().filename(), module_config.do_not_close(), - module_config.load_globally()); - if (!dynamic_module.ok()) { - return absl::InvalidArgumentError("Failed to load dynamic module: " + - std::string(dynamic_module.status().message())); - } - - return createFilterFactoryFromModule(std::move(dynamic_module.value()), proto_config, - metrics_namespace, context, scope); - } - - if (async_source.has_remote()) { - const auto& remote_source = async_source.remote(); - const std::string& sha256_hash = remote_source.sha256(); - - if (sha256_hash.empty()) { - return absl::InvalidArgumentError("SHA256 hash is required for remote module sources"); - } - - // Warming mode: block server init until the fetch completes. The init manager will - // not transition to Initialized until the RemoteAsyncDataProvider signals ready(). - if (init_manager == nullptr) { - return absl::InvalidArgumentError( - "Init manager required for warming mode with remote module sources"); - } - - // AsyncLoadState is shared between the fetch callback (which populates filter_config) - // and the returned factory callback (which reads it). Also prevents the - // RemoteAsyncDataProvider from being destroyed before the fetch completes. - struct AsyncLoadState { - Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr filter_config; - RemoteAsyncDataProviderPtr remote_provider; - }; - auto state = std::make_shared(); - - // SHA256 verification is handled by the underlying RemoteDataFetcher. - // Capture a weak_ptr to break the reference cycle: state owns remote_provider, - // and remote_provider's callback would otherwise prevent state from being freed. - std::weak_ptr weak_state = state; - state->remote_provider = std::make_unique( - context.clusterManager(), *init_manager, remote_source, context.mainThreadDispatcher(), - context.api().randomGenerator(), false, - [weak_state, sha256_hash, proto_config_copy = proto_config, &context, &scope, - metrics_namespace](const std::string& data) { - if (data.empty()) { - ENVOY_LOG_MISC(warn, "Remote dynamic module fetch failed for SHA256 {}", sha256_hash); - return; - } - auto state = weak_state.lock(); - if (!state) { - return; - } - auto filter_config = - createFilterConfigFromBytes(data, sha256_hash, proto_config_copy, context, scope); - if (!filter_config.ok()) { - ENVOY_LOG_MISC(warn, - "Remote dynamic module fetched but failed to load for SHA256 {}: {}", - sha256_hash, filter_config.status().message()); + // AsyncLoadState is shared between the fetch callback (which populates filter_config) + // and the returned factory callback (which reads it). Also owns the RemoteAsyncDataProvider + // to prevent it from being destroyed before the fetch completes. + struct AsyncLoadState { + Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr filter_config; + RemoteAsyncDataProviderPtr remote_provider; + }; + auto state = std::make_shared(); + + // SHA256 verification is handled by the underlying RemoteDataFetcher. + // Use a weak_ptr in the callback to guard against the callback firing after the + // factory lambda (which owns `state` via shared_ptr) has been destroyed. + std::weak_ptr weak_state = state; + state->remote_provider = std::make_unique( + context.clusterManager(), *init_manager, remote_source, context.mainThreadDispatcher(), + context.api().randomGenerator(), false, + [weak_state, sha256_hash, proto_config_copy = proto_config, &context, &scope, + metrics_namespace](const std::string& data) { + if (data.empty()) { + ENVOY_LOG_MISC(warn, "Remote dynamic module fetch failed for SHA256 {}", sha256_hash); + return; + } + auto state = weak_state.lock(); + if (!state) { + return; + } + + const auto& module_config = proto_config_copy.dynamic_module_config(); + auto dynamic_module = Extensions::DynamicModules::newDynamicModuleFromBytes( + data, sha256_hash, module_config.do_not_close(), module_config.load_globally()); + if (!dynamic_module.ok()) { + ENVOY_LOG_MISC(warn, "Remote dynamic module fetched but failed to load for SHA256 {}: {}", + sha256_hash, dynamic_module.status().message()); + return; + } + + std::string config; + if (proto_config_copy.has_filter_config()) { + auto config_or_error = MessageUtil::anyToBytes(proto_config_copy.filter_config()); + if (!config_or_error.ok()) { + ENVOY_LOG_MISC(warn, "Failed to parse filter config for SHA256 {}: {}", sha256_hash, + config_or_error.status().message()); return; } - state->filter_config = filter_config.value(); - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - } - }); - - // If the fetch failed, filter_config will be null and we skip (fail-open). - return [state](Http::FilterChainFactoryCallbacks& callbacks) -> void { - if (!state->filter_config) { - ENVOY_LOG_MISC(warn, - "Dynamic module filter skipped: remote module was not loaded (fail-open)"); - return; - } - createFilterFactoryCallback(state->filter_config)(callbacks); - }; - } - - return absl::InvalidArgumentError("Invalid AsyncDataSource: neither local nor remote specified"); + config = std::move(config_or_error.value()); + } + + auto filter_config = + Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( + proto_config_copy.filter_name(), config, metrics_namespace, + proto_config_copy.terminal_filter(), std::move(dynamic_module.value()), scope, + context); + if (!filter_config.ok()) { + ENVOY_LOG_MISC(warn, + "Remote dynamic module loaded but failed to create config for " + "SHA256 {}: {}", + sha256_hash, filter_config.status().message()); + return; + } + state->filter_config = filter_config.value(); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { + context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); + } + }); + + // If the fetch failed, filter_config will be null and we skip (fail-open). + return [state](Http::FilterChainFactoryCallbacks& callbacks) -> void { + if (!state->filter_config) { + ENVOY_LOG_MISC(warn, + "Dynamic module filter skipped: remote module was not loaded (fail-open)"); + return; + } + const auto& config = state->filter_config; + const std::string& worker_name = callbacks.dispatcher().name(); + auto pos = worker_name.find_first_of('_'); + ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); + uint32_t worker_index; + if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { + IS_ENVOY_BUG("failed to parse worker index from name"); + } + auto filter = + std::make_shared( + config, config->stats_scope_->symbolTable(), worker_index); + callbacks.addStreamFilter(filter); + // addStreamFilter() sets decoder/encoder filter callbacks. Initialize the in-module filter + // after so it can access all necessary context during creation. + filter->initializeInModuleFilter(); + }; } Envoy::Http::FilterFactoryCb diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index e7ed806558bf7..c8a940d04af1d 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -53,9 +53,9 @@ class DynamicModuleConfigFactory private: absl::StatusOr - createFilterFactoryFromAsyncDataSource(const FilterConfig& proto_config, - Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope, Init::Manager* init_manager); + createFilterFactoryFromRemoteSource(const FilterConfig& proto_config, + Server::Configuration::ServerFactoryContext& context, + Stats::Scope& scope, Init::Manager* init_manager); }; using UpstreamDynamicModuleConfigFactory = DynamicModuleConfigFactory; diff --git a/test/extensions/dynamic_modules/dynamic_modules_test.cc b/test/extensions/dynamic_modules/dynamic_modules_test.cc index ac28b50b79974..eeb143605dea5 100644 --- a/test/extensions/dynamic_modules/dynamic_modules_test.cc +++ b/test/extensions/dynamic_modules/dynamic_modules_test.cc @@ -306,8 +306,7 @@ TEST(CreateDynamicModulesFromBytes, TempFilePermissions) { EXPECT_TRUE(module.ok()) << module.status().message(); const std::filesystem::path temp_file = - std::filesystem::temp_directory_path() / - fmt::format("envoy_dynmod_{}.so", expected_hash); + std::filesystem::temp_directory_path() / fmt::format("envoy_dynmod_{}.so", expected_hash); EXPECT_TRUE(std::filesystem::exists(temp_file)); // Verify 0600 permissions (no group/other access). diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index e0b584da0aa1f..7c4c1fbb30a80 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -224,7 +224,7 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { const std::string worker_name = "worker_0"; NiceMock worker_dispatcher(worker_name); ON_CALL(filter_callback, dispatcher()).WillByDefault(ReturnRef(worker_dispatcher)); - EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(1); + EXPECT_CALL(filter_callback, addStreamFilter(_)); cb_or_error.value()(filter_callback); } @@ -395,9 +395,9 @@ TEST_F(DynamicModuleFilterConfigTest, ServerContextRemoteNoInitManager) { TestUtility::loadFromYaml(yaml, proto_config); DynamicModuleConfigFactory factory; - EXPECT_THROW(factory.createFilterFactoryFromProtoWithServerContextTyped( - proto_config, "stats", context_.server_factory_context_), - EnvoyException); + EXPECT_THROW_WITH_REGEX(factory.createFilterFactoryFromProtoWithServerContextTyped( + proto_config, "stats", context_.server_factory_context_), + EnvoyException, "not supported via the server context factory path"); } TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigPerRouteConfigFail) { @@ -526,8 +526,8 @@ TEST_F(DynamicModuleFilterConfigTest, ModulePrecedenceOverName) { TestUtility::loadFromYaml(yaml, proto_config); DynamicModuleConfigFactory factory; - // If name were used, this would fail because "nonexistent_module_should_be_ignored" doesn't exist. - // Since module takes precedence, it should succeed with the local file. + // If name were used, this would fail because "nonexistent_module_should_be_ignored" doesn't + // exist. Since module takes precedence, it should succeed with the local file. auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); } From 19cb20a9104704e8e1b6279aee04451821f4d15e Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 19:40:42 +0530 Subject: [PATCH 16/21] reuse methods in factory Signed-off-by: Anurag Aggarwal --- source/extensions/dynamic_modules/BUILD | 3 +- .../dynamic_modules/dynamic_modules.cc | 11 ++-- .../filters/http/dynamic_modules/factory.cc | 57 +++++++++---------- .../filters/http/dynamic_modules/factory.h | 6 ++ 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index 78c0c01ab33d0..f3f4137e3cd84 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -17,9 +17,10 @@ envoy_cc_library( ], deps = [ "//envoy/common:exception_lib", + "//source/common/buffer:buffer_lib", "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", "//source/extensions/dynamic_modules/abi", - "@boringssl//:ssl", ], ) diff --git a/source/extensions/dynamic_modules/dynamic_modules.cc b/source/extensions/dynamic_modules/dynamic_modules.cc index e5b48bb9d26fe..85c61e70a09ca 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.cc +++ b/source/extensions/dynamic_modules/dynamic_modules.cc @@ -7,11 +7,12 @@ #include "envoy/common/exception.h" +#include "source/common/buffer/buffer_impl.h" #include "source/common/common/hex.h" +#include "source/common/crypto/utility.h" #include "source/extensions/dynamic_modules/abi/abi.h" #include "absl/strings/str_cat.h" -#include "openssl/sha.h" namespace Envoy { namespace Extensions { @@ -158,10 +159,10 @@ absl::StatusOr newDynamicModuleFromBytes(absl::string_view mod return absl::InvalidArgumentError("Module bytes cannot be empty"); } - // Compute SHA256 hash of the module bytes directly without copying into a Buffer. - uint8_t digest[SHA256_DIGEST_LENGTH]; - SHA256(reinterpret_cast(module_bytes.data()), module_bytes.size(), digest); - const std::string computed_hash = Hex::encode(digest, SHA256_DIGEST_LENGTH); + // Compute SHA256 hash of the module bytes. + Buffer::OwnedImpl buffer(module_bytes); + const std::string computed_hash = + Hex::encode(Common::Crypto::UtilitySingleton::get().getSha256Digest(buffer)); // Verify SHA256 hash if provided. if (!sha256_hash.empty() && computed_hash != sha256_hash) { diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index eca0959d7b0e8..8ae5eb4840a93 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -68,29 +68,39 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa std::string(filter_config.status().message())); } + // When the runtime guard is enabled, register the metrics namespace as a custom stat namespace. + // This causes the namespace prefix to be stripped from prometheus output and no envoy_ prefix + // is added. This is the legacy behavior for backward compatibility. if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); } return [config = filter_config.value()](Http::FilterChainFactoryCallbacks& callbacks) -> void { - const std::string& worker_name = callbacks.dispatcher().name(); - auto pos = worker_name.find_first_of('_'); - ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); - uint32_t worker_index; - if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { - IS_ENVOY_BUG("failed to parse worker index from name"); - } - auto filter = - std::make_shared( - config, config->stats_scope_->symbolTable(), worker_index); - callbacks.addStreamFilter(filter); - // addStreamFilter() sets decoder/encoder filter callbacks. Initialize the in-module filter - // after so it can access all necessary context during creation. - filter->initializeInModuleFilter(); + installFilter(config, callbacks); }; } +void DynamicModuleConfigFactory::installFilter( + const Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr& config, + Http::FilterChainFactoryCallbacks& callbacks) { + const std::string& worker_name = callbacks.dispatcher().name(); + auto pos = worker_name.find_first_of('_'); + ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); + uint32_t worker_index; + if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { + IS_ENVOY_BUG("failed to parse worker index from name"); + } + auto filter = + std::make_shared( + config, config->stats_scope_->symbolTable(), worker_index); + // The addStreamFilter() will call the setDecoderFilterCallbacks first then + // setEncoderFilterCallbacks. We initialize the in-module filter after we have both callbacks + // to ensure the in-module filter can access all the necessary information during creation. + callbacks.addStreamFilter(filter); + filter->initializeInModuleFilter(); +} + absl::StatusOr DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, @@ -176,6 +186,9 @@ DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( return; } state->filter_config = filter_config.value(); + // When the runtime guard is enabled, register the metrics namespace as a custom stat + // namespace. This causes the namespace prefix to be stripped from prometheus output and + // no envoy_ prefix is added. This is the legacy behavior for backward compatibility. if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); @@ -189,21 +202,7 @@ DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( "Dynamic module filter skipped: remote module was not loaded (fail-open)"); return; } - const auto& config = state->filter_config; - const std::string& worker_name = callbacks.dispatcher().name(); - auto pos = worker_name.find_first_of('_'); - ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); - uint32_t worker_index; - if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { - IS_ENVOY_BUG("failed to parse worker index from name"); - } - auto filter = - std::make_shared( - config, config->stats_scope_->symbolTable(), worker_index); - callbacks.addStreamFilter(filter); - // addStreamFilter() sets decoder/encoder filter callbacks. Initialize the in-module filter - // after so it can access all necessary context during creation. - filter->initializeInModuleFilter(); + installFilter(state->filter_config, callbacks); }; } diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index c8a940d04af1d..1eecfdd7441e6 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -52,6 +52,12 @@ class DynamicModuleConfigFactory } private: + // Creates a filter and installs it on the filter chain. Shared by both the sync and async + // (remote) factory paths. + static void installFilter( + const Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr& config, + Http::FilterChainFactoryCallbacks& callbacks); + absl::StatusOr createFilterFactoryFromRemoteSource(const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, From d134e6a1375178c47ce602a9dbb397cb121d474f Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 19:56:05 +0530 Subject: [PATCH 17/21] add exec permissions to temp file Signed-off-by: Anurag Aggarwal --- source/extensions/dynamic_modules/dynamic_modules.cc | 6 ++---- test/extensions/dynamic_modules/dynamic_modules_test.cc | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/source/extensions/dynamic_modules/dynamic_modules.cc b/source/extensions/dynamic_modules/dynamic_modules.cc index 85c61e70a09ca..b28fee3c7acba 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.cc +++ b/source/extensions/dynamic_modules/dynamic_modules.cc @@ -195,11 +195,9 @@ absl::StatusOr newDynamicModuleFromBytes(absl::string_view mod } ofs.close(); - // Set file permissions to 0600 (owner read/write only). + // Set file permissions to 0700 (owner only). dlopen requires read+exec. std::error_code ec; - std::filesystem::permissions( - temp_file_writing, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, - ec); + std::filesystem::permissions(temp_file_writing, std::filesystem::perms::owner_all, ec); if (ec) { std::filesystem::remove(temp_file_writing); return absl::InternalError( diff --git a/test/extensions/dynamic_modules/dynamic_modules_test.cc b/test/extensions/dynamic_modules/dynamic_modules_test.cc index eeb143605dea5..0e950c27a1348 100644 --- a/test/extensions/dynamic_modules/dynamic_modules_test.cc +++ b/test/extensions/dynamic_modules/dynamic_modules_test.cc @@ -309,10 +309,9 @@ TEST(CreateDynamicModulesFromBytes, TempFilePermissions) { std::filesystem::temp_directory_path() / fmt::format("envoy_dynmod_{}.so", expected_hash); EXPECT_TRUE(std::filesystem::exists(temp_file)); - // Verify 0600 permissions (no group/other access). + // Verify 0700 permissions (owner only, no group/other access). auto perms = std::filesystem::status(temp_file).permissions(); - EXPECT_EQ(perms & std::filesystem::perms::group_all, std::filesystem::perms::none); - EXPECT_EQ(perms & std::filesystem::perms::others_all, std::filesystem::perms::none); + EXPECT_EQ(perms, std::filesystem::perms::owner_all); } } // namespace DynamicModules From c838d3bdb6d41f0b004baf5ebf1499faaf4f9bf6 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Tue, 3 Mar 2026 20:01:13 +0530 Subject: [PATCH 18/21] add owner_all permissions to temp file Signed-off-by: Anurag Aggarwal --- .../filters/http/dynamic_modules/factory.cc | 49 ++++++++++--------- .../filters/http/dynamic_modules/factory.h | 6 --- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index 8ae5eb4840a93..e81d61ae1d7a6 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -77,30 +77,23 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa } return [config = filter_config.value()](Http::FilterChainFactoryCallbacks& callbacks) -> void { - installFilter(config, callbacks); + const std::string& worker_name = callbacks.dispatcher().name(); + auto pos = worker_name.find_first_of('_'); + ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); + uint32_t worker_index; + if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { + IS_ENVOY_BUG("failed to parse worker index from name"); + } + auto filter = + std::make_shared( + config, config->stats_scope_->symbolTable(), worker_index); + callbacks.addStreamFilter(filter); + // addStreamFilter() sets decoder/encoder filter callbacks. Initialize the in-module filter + // after so it can access all necessary context during creation. + filter->initializeInModuleFilter(); }; } -void DynamicModuleConfigFactory::installFilter( - const Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr& config, - Http::FilterChainFactoryCallbacks& callbacks) { - const std::string& worker_name = callbacks.dispatcher().name(); - auto pos = worker_name.find_first_of('_'); - ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); - uint32_t worker_index; - if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { - IS_ENVOY_BUG("failed to parse worker index from name"); - } - auto filter = - std::make_shared( - config, config->stats_scope_->symbolTable(), worker_index); - // The addStreamFilter() will call the setDecoderFilterCallbacks first then - // setEncoderFilterCallbacks. We initialize the in-module filter after we have both callbacks - // to ensure the in-module filter can access all the necessary information during creation. - callbacks.addStreamFilter(filter); - filter->initializeInModuleFilter(); -} - absl::StatusOr DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, @@ -202,7 +195,19 @@ DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( "Dynamic module filter skipped: remote module was not loaded (fail-open)"); return; } - installFilter(state->filter_config, callbacks); + const auto& config = state->filter_config; + const std::string& worker_name = callbacks.dispatcher().name(); + auto pos = worker_name.find_first_of('_'); + ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); + uint32_t worker_index; + if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { + IS_ENVOY_BUG("failed to parse worker index from name"); + } + auto filter = + std::make_shared( + config, config->stats_scope_->symbolTable(), worker_index); + callbacks.addStreamFilter(filter); + filter->initializeInModuleFilter(); }; } diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index 1eecfdd7441e6..c8a940d04af1d 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -52,12 +52,6 @@ class DynamicModuleConfigFactory } private: - // Creates a filter and installs it on the filter chain. Shared by both the sync and async - // (remote) factory paths. - static void installFilter( - const Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr& config, - Http::FilterChainFactoryCallbacks& callbacks); - absl::StatusOr createFilterFactoryFromRemoteSource(const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, From 456802364c180949ffe7979376a87e6f192a2d4e Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Wed, 4 Mar 2026 15:30:13 +0530 Subject: [PATCH 19/21] try keeping local.filename support only Signed-off-by: Anurag Aggarwal --- .../dynamic_modules/v3/dynamic_modules.proto | 6 +- changelogs/current.yaml | 9 +- source/extensions/dynamic_modules/BUILD | 3 - .../dynamic_modules/dynamic_modules.cc | 72 ---- .../dynamic_modules/dynamic_modules.h | 14 - .../filters/http/dynamic_modules/BUILD | 2 - .../filters/http/dynamic_modules/factory.cc | 134 +----- .../filters/http/dynamic_modules/factory.h | 16 +- test/coverage.yaml | 3 +- test/extensions/dynamic_modules/BUILD | 3 - .../dynamic_modules/dynamic_modules_test.cc | 123 ------ .../filters/http/dynamic_modules/BUILD | 7 - .../http/dynamic_modules/config_test.cc | 384 +----------------- 13 files changed, 27 insertions(+), 749 deletions(-) diff --git a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto index f0ac00028d142..d4e4d9a7bcde0 100644 --- a/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto +++ b/api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto @@ -85,10 +85,8 @@ message DynamicModuleConfig { // Defaults to ``dynamicmodulescustom``. string metrics_namespace = 5; - // The dynamic module binary to load. Currently supports local file paths - // (via ``local.filename``) and remote HTTP URLs (via ``remote``). - // - // For remote sources, the ``sha256`` field is required for integrity verification. + // The dynamic module binary to load. Currently only supports local file paths + // via ``local.filename``. // // When both ``name`` and ``module`` are set, ``module`` takes precedence. config.core.v3.AsyncDataSource module = 6; diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 75666b14b0373..cfa358754d2d4 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -460,11 +460,10 @@ new_features: cross-module interactions analogous to ``dlsym``. - area: dynamic_modules change: | - Added support for loading dynamic module binaries from local file paths or remote - URLs via the new :ref:`module - ` field in - ``DynamicModuleConfig``. For remote sources, SHA256 verification is required. The server - blocks during initialization until the remote fetch completes. + Added support for loading dynamic module binaries from local file paths via the new + :ref:`module ` + field in ``DynamicModuleConfig``. This allows specifying an absolute path to a ``.so`` file + via ``module.local.filename`` as an alternative to the name-based search path. - area: dynamic_modules change: | Network filter read and write buffers now persist after ``on_read``/``on_write`` callbacks, allowing diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index f3f4137e3cd84..fb3630b74ac11 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -17,9 +17,6 @@ envoy_cc_library( ], deps = [ "//envoy/common:exception_lib", - "//source/common/buffer:buffer_lib", - "//source/common/common:hex_lib", - "//source/common/crypto:utility_lib", "//source/extensions/dynamic_modules/abi", ], ) diff --git a/source/extensions/dynamic_modules/dynamic_modules.cc b/source/extensions/dynamic_modules/dynamic_modules.cc index b28fee3c7acba..eea35289e965a 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.cc +++ b/source/extensions/dynamic_modules/dynamic_modules.cc @@ -2,14 +2,10 @@ #include -#include #include #include "envoy/common/exception.h" -#include "source/common/buffer/buffer_impl.h" -#include "source/common/common/hex.h" -#include "source/common/crypto/utility.h" #include "source/extensions/dynamic_modules/abi/abi.h" #include "absl/strings/str_cat.h" @@ -152,74 +148,6 @@ void* DynamicModule::getSymbol(const absl::string_view symbol_ref) const { return dlsym(handle_, std::string(symbol_ref).c_str()); } -absl::StatusOr newDynamicModuleFromBytes(absl::string_view module_bytes, - absl::string_view sha256_hash, - bool do_not_close, bool load_globally) { - if (module_bytes.empty()) { - return absl::InvalidArgumentError("Module bytes cannot be empty"); - } - - // Compute SHA256 hash of the module bytes. - Buffer::OwnedImpl buffer(module_bytes); - const std::string computed_hash = - Hex::encode(Common::Crypto::UtilitySingleton::get().getSha256Digest(buffer)); - - // Verify SHA256 hash if provided. - if (!sha256_hash.empty() && computed_hash != sha256_hash) { - return absl::InvalidArgumentError( - absl::StrCat("SHA256 hash mismatch: expected ", sha256_hash, ", got ", computed_hash)); - } - - // Use computed_hash for the temp file name (if sha256_hash was provided and matched, - // computed_hash == sha256_hash at this point). - const std::filesystem::path temp_dir = std::filesystem::temp_directory_path(); - const std::filesystem::path temp_file_path = - temp_dir / fmt::format("envoy_dynmod_{}.so", computed_hash); - - if (!std::filesystem::exists(temp_file_path)) { - // Write to a pid-suffixed temp file first, then atomically rename to avoid partial reads. - const std::filesystem::path temp_file_writing = - temp_dir / fmt::format("envoy_dynmod_{}.so.tmp.{}", computed_hash, getpid()); - - // Write the module bytes to the temp file with secure permissions. - std::ofstream ofs(temp_file_writing, std::ios::binary | std::ios::trunc); - if (!ofs) { - return absl::InternalError( - absl::StrCat("Failed to create temp file: ", temp_file_writing.string())); - } - ofs.write(module_bytes.data(), module_bytes.size()); - if (!ofs) { - std::filesystem::remove(temp_file_writing); - return absl::InternalError( - absl::StrCat("Failed to write module bytes to temp file: ", temp_file_writing.string())); - } - ofs.close(); - - // Set file permissions to 0700 (owner only). dlopen requires read+exec. - std::error_code ec; - std::filesystem::permissions(temp_file_writing, std::filesystem::perms::owner_all, ec); - if (ec) { - std::filesystem::remove(temp_file_writing); - return absl::InternalError( - absl::StrCat("Failed to set permissions on temp file: ", ec.message())); - } - - // Atomically rename the temp file to the final path. - std::filesystem::rename(temp_file_writing, temp_file_path, ec); - if (ec) { - // If rename fails (e.g., another process created the file), that's OK - use the existing - // file. - std::filesystem::remove(temp_file_writing); - if (!std::filesystem::exists(temp_file_path)) { - return absl::InternalError(absl::StrCat("Failed to rename temp file: ", ec.message())); - } - } - } - - // Load the module from the temp file. - return newDynamicModule(temp_file_path, do_not_close, load_globally); -} - absl::StatusOr newStaticModule(const absl::string_view module_name) { auto dynamic_module = std::make_unique(module_name); diff --git a/source/extensions/dynamic_modules/dynamic_modules.h b/source/extensions/dynamic_modules/dynamic_modules.h index b997216cb0e0d..37f13a845d41d 100644 --- a/source/extensions/dynamic_modules/dynamic_modules.h +++ b/source/extensions/dynamic_modules/dynamic_modules.h @@ -99,20 +99,6 @@ absl::StatusOr newDynamicModuleByName(const absl::string_view const bool do_not_close, const bool load_globally = false); -/** - * Creates a new DynamicModule from in-memory bytes. The bytes are written to a SHA256-named - * temporary file (for deduplication) before loading via dlopen. - * @param module_bytes the raw bytes of the dynamic module (.so file). - * @param sha256_hash the expected SHA256 hash for verification. If empty, hash is computed but - * not verified. - * @param do_not_close if true, uses RTLD_NODELETE. - * @param load_globally if true, uses RTLD_GLOBAL. - */ -absl::StatusOr newDynamicModuleFromBytes(absl::string_view module_bytes, - absl::string_view sha256_hash, - bool do_not_close, - bool load_globally = false); - /** * Creates a new DynamicModule backed by symbols already present in the process binary (i.e., * statically linked). No shared object file is loaded. Instead, symbols are resolved via diff --git a/source/extensions/filters/http/dynamic_modules/BUILD b/source/extensions/filters/http/dynamic_modules/BUILD index 584b1a7407387..286e6cb1a26e2 100644 --- a/source/extensions/filters/http/dynamic_modules/BUILD +++ b/source/extensions/filters/http/dynamic_modules/BUILD @@ -38,8 +38,6 @@ envoy_cc_library( ":abi_impl", ":filter_config_lib", ":filter_lib", - "//envoy/init:manager_interface", - "//source/extensions/common/wasm:remote_async_datasource_lib", "//source/extensions/dynamic_modules:dynamic_modules_lib", "//source/extensions/filters/http/common:factory_base_lib", "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index e81d61ae1d7a6..d201e775dccd4 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -10,22 +10,16 @@ namespace Configuration { absl::StatusOr DynamicModuleConfigFactory::createFilterFactory( const FilterConfig& proto_config, const std::string&, - Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, - Init::Manager* init_manager) { + Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope) { const auto& module_config = proto_config.dynamic_module_config(); - // Remote source requires async warming — handle separately. - if (module_config.has_module() && module_config.module().has_remote()) { - return createFilterFactoryFromRemoteSource(proto_config, context, scope, init_manager); - } - // Load the module: either from a local file path or by name. absl::StatusOr dynamic_module; if (module_config.has_module()) { if (!module_config.module().has_local() || !module_config.module().local().has_filename()) { - return absl::InvalidArgumentError("Only local.filename is supported for module sources; " - "inline_bytes and inline_string are not supported"); + return absl::InvalidArgumentError( + "Only local file path is supported for module sources (via module.local.filename)"); } dynamic_module = Extensions::DynamicModules::newDynamicModule( module_config.module().local().filename(), module_config.do_not_close(), @@ -88,125 +82,11 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa std::make_shared( config, config->stats_scope_->symbolTable(), worker_index); callbacks.addStreamFilter(filter); - // addStreamFilter() sets decoder/encoder filter callbacks. Initialize the in-module filter - // after so it can access all necessary context during creation. - filter->initializeInModuleFilter(); - }; -} - -absl::StatusOr -DynamicModuleConfigFactory::createFilterFactoryFromRemoteSource( - const FilterConfig& proto_config, Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope, Init::Manager* init_manager) { - - const auto& module_config = proto_config.dynamic_module_config(); - const auto& remote_source = module_config.module().remote(); - const std::string& sha256_hash = remote_source.sha256(); - - if (sha256_hash.empty()) { - return absl::InvalidArgumentError("SHA256 hash is required for remote module sources"); - } - - if (init_manager == nullptr) { - return absl::InvalidArgumentError( - "Remote module sources require an init manager for warming and are not supported via " - "the server context factory path (createFilterFactoryFromProtoWithServerContext). " - "Use the listener-level filter config instead"); - } - - const std::string metrics_namespace = - module_config.metrics_namespace().empty() - ? std::string(Extensions::DynamicModules::HttpFilters::DefaultMetricsNamespace) - : module_config.metrics_namespace(); - // AsyncLoadState is shared between the fetch callback (which populates filter_config) - // and the returned factory callback (which reads it). Also owns the RemoteAsyncDataProvider - // to prevent it from being destroyed before the fetch completes. - struct AsyncLoadState { - Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr filter_config; - RemoteAsyncDataProviderPtr remote_provider; - }; - auto state = std::make_shared(); - - // SHA256 verification is handled by the underlying RemoteDataFetcher. - // Use a weak_ptr in the callback to guard against the callback firing after the - // factory lambda (which owns `state` via shared_ptr) has been destroyed. - std::weak_ptr weak_state = state; - state->remote_provider = std::make_unique( - context.clusterManager(), *init_manager, remote_source, context.mainThreadDispatcher(), - context.api().randomGenerator(), false, - [weak_state, sha256_hash, proto_config_copy = proto_config, &context, &scope, - metrics_namespace](const std::string& data) { - if (data.empty()) { - ENVOY_LOG_MISC(warn, "Remote dynamic module fetch failed for SHA256 {}", sha256_hash); - return; - } - auto state = weak_state.lock(); - if (!state) { - return; - } - - const auto& module_config = proto_config_copy.dynamic_module_config(); - auto dynamic_module = Extensions::DynamicModules::newDynamicModuleFromBytes( - data, sha256_hash, module_config.do_not_close(), module_config.load_globally()); - if (!dynamic_module.ok()) { - ENVOY_LOG_MISC(warn, "Remote dynamic module fetched but failed to load for SHA256 {}: {}", - sha256_hash, dynamic_module.status().message()); - return; - } - - std::string config; - if (proto_config_copy.has_filter_config()) { - auto config_or_error = MessageUtil::anyToBytes(proto_config_copy.filter_config()); - if (!config_or_error.ok()) { - ENVOY_LOG_MISC(warn, "Failed to parse filter config for SHA256 {}: {}", sha256_hash, - config_or_error.status().message()); - return; - } - config = std::move(config_or_error.value()); - } - - auto filter_config = - Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - proto_config_copy.filter_name(), config, metrics_namespace, - proto_config_copy.terminal_filter(), std::move(dynamic_module.value()), scope, - context); - if (!filter_config.ok()) { - ENVOY_LOG_MISC(warn, - "Remote dynamic module loaded but failed to create config for " - "SHA256 {}: {}", - sha256_hash, filter_config.status().message()); - return; - } - state->filter_config = filter_config.value(); - // When the runtime guard is enabled, register the metrics namespace as a custom stat - // namespace. This causes the namespace prefix to be stripped from prometheus output and - // no envoy_ prefix is added. This is the legacy behavior for backward compatibility. - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.dynamic_modules_strip_custom_stat_prefix")) { - context.api().customStatNamespaces().registerStatNamespace(metrics_namespace); - } - }); - - // If the fetch failed, filter_config will be null and we skip (fail-open). - return [state](Http::FilterChainFactoryCallbacks& callbacks) -> void { - if (!state->filter_config) { - ENVOY_LOG_MISC(warn, - "Dynamic module filter skipped: remote module was not loaded (fail-open)"); - return; - } - const auto& config = state->filter_config; - const std::string& worker_name = callbacks.dispatcher().name(); - auto pos = worker_name.find_first_of('_'); - ENVOY_BUG(pos != std::string::npos, "worker name is not in expected format worker_{index}"); - uint32_t worker_index; - if (!absl::SimpleAtoi(worker_name.substr(pos + 1), &worker_index)) { - IS_ENVOY_BUG("failed to parse worker index from name"); - } - auto filter = - std::make_shared( - config, config->stats_scope_->symbolTable(), worker_index); - callbacks.addStreamFilter(filter); + // The addStreamFilter() will call the setDecoderFilterCallbacks first then + // setEncoderFilterCallbacks. + // We can initialize the in-module filter after we have both callbacks to ensure the in module + // filter can access all the necessary information during creation. filter->initializeInModuleFilter(); }; } diff --git a/source/extensions/filters/http/dynamic_modules/factory.h b/source/extensions/filters/http/dynamic_modules/factory.h index c8a940d04af1d..825033c3523b9 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.h +++ b/source/extensions/filters/http/dynamic_modules/factory.h @@ -1,13 +1,9 @@ #pragma once -#include - #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.validate.h" -#include "envoy/init/manager.h" #include "envoy/server/filter_config.h" -#include "source/extensions/common/wasm/remote_async_datasource.h" #include "source/extensions/dynamic_modules/dynamic_modules.h" #include "source/extensions/filters/http/common/factory_base.h" @@ -27,8 +23,7 @@ class DynamicModuleConfigFactory createFilterFactoryFromProtoTyped(const FilterConfig& proto_config, const std::string& stat_prefix, DualInfo dual_info, Server::Configuration::ServerFactoryContext& context) override { - return createFilterFactory(proto_config, stat_prefix, context, dual_info.scope, - &dual_info.init_manager); + return createFilterFactory(proto_config, stat_prefix, context, dual_info.scope); } Envoy::Http::FilterFactoryCb createFilterFactoryFromProtoWithServerContextTyped( const FilterConfig& proto_config, const std::string& stat_prefix, @@ -36,8 +31,7 @@ class DynamicModuleConfigFactory absl::StatusOr createFilterFactory(const FilterConfig& proto_config, const std::string& stat_prefix, - Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope, - Init::Manager* init_manager = nullptr); + Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope); absl::StatusOr createRouteSpecificFilterConfigTyped(const RouteConfigProto&, @@ -50,12 +44,6 @@ class DynamicModuleConfigFactory Server::Configuration::ServerFactoryContext&) override { return proto_config.terminal_filter(); } - -private: - absl::StatusOr - createFilterFactoryFromRemoteSource(const FilterConfig& proto_config, - Server::Configuration::ServerFactoryContext& context, - Stats::Scope& scope, Init::Manager* init_manager); }; using UpstreamDynamicModuleConfigFactory = DynamicModuleConfigFactory; diff --git a/test/coverage.yaml b/test/coverage.yaml index f7b43a42e1e0e..5a596b548454c 100644 --- a/test/coverage.yaml +++ b/test/coverage.yaml @@ -41,7 +41,7 @@ directories: source/extensions/filters/http/a2a: 96.1 # Under active development source/extensions/filters/http/cache: 95.9 source/extensions/filters/http/dynamic_forward_proxy: 94.8 - source/extensions/filters/http/dynamic_modules: 94.5 + source/extensions/filters/http/dynamic_modules: 95.2 source/extensions/filters/http/decompressor: 95.9 source/extensions/filters/http/ext_proc: 96.4 source/extensions/filters/http/grpc_json_reverse_transcoder: 94.8 @@ -89,5 +89,4 @@ directories: source/extensions/health_checkers/grpc: 92.3 source/extensions/config_subscription/rest: 94.9 source/extensions/matching/input_matchers/cel_matcher: 100.0 - source/extensions/dynamic_modules: 92.0 # Filesystem error paths in newDynamicModuleFromBytes are hard to test without being flaky source/extensions/dynamic_modules/sdk/cpp: 0.0 # SDK code self not directly tested diff --git a/test/extensions/dynamic_modules/BUILD b/test/extensions/dynamic_modules/BUILD index 64c32e4f9928f..db8aacbbe0ec4 100644 --- a/test/extensions/dynamic_modules/BUILD +++ b/test/extensions/dynamic_modules/BUILD @@ -37,9 +37,6 @@ envoy_cc_test( rbe_pool = "6gig", deps = [ ":util", - "//source/common/buffer:buffer_lib", - "//source/common/common:hex_lib", - "//source/common/crypto:utility_lib", "//source/extensions/dynamic_modules:abi_impl", "//source/extensions/dynamic_modules:dynamic_modules_lib", "//test/extensions/dynamic_modules/test_data/c:matcher_no_op_static", diff --git a/test/extensions/dynamic_modules/dynamic_modules_test.cc b/test/extensions/dynamic_modules/dynamic_modules_test.cc index 0e950c27a1348..d8cb467439e22 100644 --- a/test/extensions/dynamic_modules/dynamic_modules_test.cc +++ b/test/extensions/dynamic_modules/dynamic_modules_test.cc @@ -1,8 +1,3 @@ -#include - -#include "source/common/buffer/buffer_impl.h" -#include "source/common/common/hex.h" -#include "source/common/crypto/utility.h" #include "source/extensions/dynamic_modules/dynamic_modules.h" #include "test/extensions/dynamic_modules/util.h" @@ -196,124 +191,6 @@ TEST(CreateDynamicModulesByName, ModuleNotFound) { "Failed to load dynamic module: libno_op.so not found in any search path")); } -// Tests for newDynamicModuleFromBytes - -TEST(CreateDynamicModulesFromBytes, EmptyBytes) { - absl::StatusOr module = newDynamicModuleFromBytes("", "", false); - EXPECT_FALSE(module.ok()); - EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); - EXPECT_THAT(module.status().message(), testing::HasSubstr("Module bytes cannot be empty")); -} - -TEST(CreateDynamicModulesFromBytes, ValidModuleNoHash) { - // Read a valid module from disk. - std::string module_path = testSharedObjectPath("no_op", "c"); - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - // Load the module from bytes without providing a hash. - absl::StatusOr module = - newDynamicModuleFromBytes(module_bytes, "", false, false); - EXPECT_TRUE(module.ok()) << "Failed to load module: " << module.status().message(); -} - -TEST(CreateDynamicModulesFromBytes, ValidModuleWithCorrectHash) { - // Read a valid module from disk. - std::string module_path = testSharedObjectPath("no_op", "c"); - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - // Compute the expected SHA256 hash. - auto& crypto_util = Common::Crypto::UtilitySingleton::get(); - Buffer::OwnedImpl buffer(module_bytes); - std::string expected_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); - - // Load the module from bytes with the correct hash. - absl::StatusOr module = - newDynamicModuleFromBytes(module_bytes, expected_hash, false, false); - EXPECT_TRUE(module.ok()) << "Failed to load module: " << module.status().message(); -} - -TEST(CreateDynamicModulesFromBytes, ValidModuleWithIncorrectHash) { - // Read a valid module from disk. - std::string module_path = testSharedObjectPath("no_op", "c"); - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - // Try to load the module with an incorrect hash. - absl::StatusOr module = - newDynamicModuleFromBytes(module_bytes, "incorrect_hash", false, false); - EXPECT_FALSE(module.ok()); - EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); - EXPECT_THAT(module.status().message(), testing::HasSubstr("SHA256 hash mismatch")); -} - -TEST(CreateDynamicModulesFromBytes, TempFileDeduplication) { - // Read a valid module from disk. - std::string module_path = testSharedObjectPath("no_op", "c"); - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - // Load the module twice with the same bytes. - absl::StatusOr module1 = - newDynamicModuleFromBytes(module_bytes, "", false, false); - EXPECT_TRUE(module1.ok()) << "Failed to load module1: " << module1.status().message(); - - absl::StatusOr module2 = - newDynamicModuleFromBytes(module_bytes, "", false, false); - EXPECT_TRUE(module2.ok()) << "Failed to load module2: " << module2.status().message(); - - // Both should succeed and point to the same underlying module (via dlopen deduplication). -} - -TEST(CreateDynamicModulesFromBytes, InvalidModuleBytes) { - // Try to load invalid bytes as a module. - std::string invalid_bytes = "this is not a valid shared object"; - absl::StatusOr module = - newDynamicModuleFromBytes(invalid_bytes, "", false, false); - EXPECT_FALSE(module.ok()); - // The error should come from dlopen failing to load the invalid file. - EXPECT_EQ(module.status().code(), absl::StatusCode::kInvalidArgument); -} - -// Verify the temp file is created at expected path with restrictive permissions. -TEST(CreateDynamicModulesFromBytes, TempFilePermissions) { - std::string module_path = testSharedObjectPath("no_op", "c"); - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - auto& crypto_util = Common::Crypto::UtilitySingleton::get(); - Buffer::OwnedImpl buffer(module_bytes); - std::string expected_hash = Hex::encode(crypto_util.getSha256Digest(buffer)); - - absl::StatusOr module = - newDynamicModuleFromBytes(module_bytes, expected_hash, false, false); - EXPECT_TRUE(module.ok()) << module.status().message(); - - const std::filesystem::path temp_file = - std::filesystem::temp_directory_path() / fmt::format("envoy_dynmod_{}.so", expected_hash); - EXPECT_TRUE(std::filesystem::exists(temp_file)); - - // Verify 0700 permissions (owner only, no group/other access). - auto perms = std::filesystem::status(temp_file).permissions(); - EXPECT_EQ(perms, std::filesystem::perms::owner_all); -} - } // namespace DynamicModules } // namespace Extensions } // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD index fbd929a6faa55..7c35a6da2e533 100644 --- a/test/extensions/filters/http/dynamic_modules/BUILD +++ b/test/extensions/filters/http/dynamic_modules/BUILD @@ -12,20 +12,13 @@ envoy_cc_test( name = "config_test", srcs = ["config_test.cc"], data = [ - "//test/extensions/dynamic_modules/test_data/c:http_filter_per_route_config_new_fail", "//test/extensions/dynamic_modules/test_data/c:no_op", ], deps = [ - "//source/common/buffer:buffer_lib", - "//source/common/common:hex_lib", - "//source/common/crypto:utility_lib", - "//source/common/http:message_lib", "//source/common/stats:isolated_store_lib", "//source/extensions/filters/http/dynamic_modules:factory_lib", "//test/extensions/dynamic_modules:util", - "//test/mocks/http:http_mocks", "//test/mocks/network:network_mocks", - "//test/mocks/protobuf:protobuf_mocks", "//test/mocks/server:server_mocks", "//test/test_common:environment_lib", "//test/test_common:simulated_time_system_lib", diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/filters/http/dynamic_modules/config_test.cc index 7c4c1fbb30a80..ff22b5162b53e 100644 --- a/test/extensions/filters/http/dynamic_modules/config_test.cc +++ b/test/extensions/filters/http/dynamic_modules/config_test.cc @@ -1,19 +1,11 @@ -#include - #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.validate.h" -#include "source/common/buffer/buffer_impl.h" -#include "source/common/common/hex.h" -#include "source/common/crypto/utility.h" -#include "source/common/http/message_impl.h" #include "source/common/stats/isolated_store_impl.h" #include "source/extensions/filters/http/dynamic_modules/factory.h" #include "test/extensions/dynamic_modules/util.h" -#include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" -#include "test/mocks/protobuf/mocks.h" #include "test/mocks/server/mocks.h" #include "test/test_common/environment.h" #include "test/test_common/utility.h" @@ -21,7 +13,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -using testing::_; using testing::ReturnRef; namespace Envoy { @@ -36,10 +27,6 @@ class DynamicModuleFilterConfigTest : public Event::TestUsingSimulatedTime, publ ON_CALL(context_, listenerInfo()).WillByDefault(ReturnRef(listener_info_)); ON_CALL(listener_info_, metadata()).WillByDefault(ReturnRef(listener_metadata_)); EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager_)); - ON_CALL(context_.server_factory_context_, clusterManager()) - .WillByDefault(ReturnRef(cluster_manager_)); - ON_CALL(context_.server_factory_context_, mainThreadDispatcher()) - .WillByDefault(ReturnRef(dispatcher_)); } NiceMock listener_info_; @@ -48,41 +35,11 @@ class DynamicModuleFilterConfigTest : public Event::TestUsingSimulatedTime, publ Api::ApiPtr api_; envoy::config::core::v3::Metadata listener_metadata_; Init::ManagerImpl init_manager_{"init_manager"}; - NiceMock cluster_manager_; Init::ExpectableWatcherImpl init_watcher_; - NiceMock dispatcher_; NiceMock context_; }; -TEST_F(DynamicModuleFilterConfigTest, LegacyNameBasedLoading) { - // Set up the search path to find the test module. - TestEnvironment::setEnvVar( - "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", - TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), - 1); - - const std::string yaml = R"EOF( - dynamic_module_config: - name: "no_op" - do_not_close: true - filter_name: "test_filter" - )EOF"; - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher_, ready()); - context_.initManager().initialize(init_watcher_); - EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); - - TestEnvironment::unsetEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH"); -} - TEST_F(DynamicModuleFilterConfigTest, LocalFileLoading) { const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); @@ -125,7 +82,7 @@ TEST_F(DynamicModuleFilterConfigTest, InlineBytesRejected) { auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); EXPECT_FALSE(cb_or_error.ok()); EXPECT_THAT(cb_or_error.status().message(), - testing::HasSubstr("Only local.filename is supported")); + testing::HasSubstr("Only local file path is supported")); } TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { @@ -145,38 +102,8 @@ TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { testing::HasSubstr("Either 'name' or 'module' must be specified")); } -TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { +TEST_F(DynamicModuleFilterConfigTest, RemoteSourceRejected) { const std::string yaml = R"EOF( - dynamic_module_config: - module: - local: - filename: "/nonexistent/path/to/module.so" - do_not_close: true - filter_name: "test_filter" - )EOF"; - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_FALSE(cb_or_error.ok()); - EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to load dynamic module")); -} - -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - const std::string yaml = absl::StrCat(R"EOF( dynamic_module_config: module: remote: @@ -184,135 +111,7 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeSuccess) { uri: https://example.com/module.so cluster: cluster_1 timeout: 5s - sha256: )EOF", - sha256, R"EOF( - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::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().add(module_bytes); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - DynamicModuleConfigFactory factory; - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher_, ready()); - init_manager_.initialize(init_watcher_); - EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); - - // Exercise the returned factory callback to verify the filter is actually installed. - NiceMock filter_callback; - const std::string worker_name = "worker_0"; - NiceMock worker_dispatcher(worker_name); - ON_CALL(filter_callback, dispatcher()).WillByDefault(ReturnRef(worker_dispatcher)); - EXPECT_CALL(filter_callback, addStreamFilter(_)); - cb_or_error.value()(filter_callback); -} - -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingWarmingModeFetchFailure) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(module_bytes))); - - // Set num_retries: 0 so RemoteAsyncDataProvider won't try to use the retry timer. - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 0 - sha256: )EOF", - sha256, R"EOF( - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::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"}}})); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - - DynamicModuleConfigFactory factory; - auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher_, ready()); - init_manager_.initialize(init_watcher_); - EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); - - // Fetch failed so the callback is a no-op (fail-open). - NiceMock filter_callback; - EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(0); - cb_or_error.value()(filter_callback); -} - -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { - const std::string module_path = Extensions::DynamicModules::testSharedObjectPath("no_op", "c"); - - std::ifstream file(module_path, std::ios::binary); - ASSERT_TRUE(file.good()) << "Failed to open test module: " << module_path; - std::string module_bytes((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - ASSERT_FALSE(module_bytes.empty()); - - // Set num_retries: 0 so RemoteAsyncDataProvider won't try to use the retry timer. - // Use an incorrect SHA256 hash that won't match the actual module bytes. - const std::string yaml = R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 0 - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + sha256: "abc123" do_not_close: true filter_name: "test_filter" )EOF"; @@ -320,73 +119,19 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingSHA256Mismatch) { envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; TestUtility::loadFromYaml(yaml, proto_config); - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::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().add(module_bytes); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher_, ready()); - init_manager_.initialize(init_watcher_); - EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); - - // RemoteDataFetcher rejects the SHA256 mismatch, so filter_config stays null (fail-open). - NiceMock filter_callback; - EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(0); - cb_or_error.value()(filter_callback); -} - -TEST_F(DynamicModuleFilterConfigTest, ServerContextFactory) { - TestEnvironment::setEnvVar( - "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", - TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), - 1); - - const std::string yaml = R"EOF( - dynamic_module_config: - name: "no_op" - do_not_close: true - filter_name: "test_filter" - )EOF"; - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - EXPECT_FALSE( - factory.isTerminalFilterByProtoTyped(proto_config, context_.server_factory_context_)); - EXPECT_NO_THROW(factory.createFilterFactoryFromProtoWithServerContextTyped( - proto_config, "stats", context_.server_factory_context_)); - - TestEnvironment::unsetEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH"); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), + testing::HasSubstr("Only local file path is supported")); } -TEST_F(DynamicModuleFilterConfigTest, ServerContextRemoteNoInitManager) { +TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { const std::string yaml = R"EOF( dynamic_module_config: module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - sha256: "abc123" + local: + filename: "/nonexistent/path/to/module.so" do_not_close: true filter_name: "test_filter" )EOF"; @@ -394,117 +139,10 @@ TEST_F(DynamicModuleFilterConfigTest, ServerContextRemoteNoInitManager) { envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; TestUtility::loadFromYaml(yaml, proto_config); - DynamicModuleConfigFactory factory; - EXPECT_THROW_WITH_REGEX(factory.createFilterFactoryFromProtoWithServerContextTyped( - proto_config, "stats", context_.server_factory_context_), - EnvoyException, "not supported via the server context factory path"); -} - -TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigPerRouteConfigFail) { - // Set up the search path to find the test module. - TestEnvironment::setEnvVar( - "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", - TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), - 1); - - // http_filter_per_route_config_new_fail exports the per-route config symbol but returns nullptr. - const std::string yaml = R"EOF( - dynamic_module_config: - name: "http_filter_per_route_config_new_fail" - do_not_close: true - per_route_config_name: "test" - )EOF"; - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilterPerRoute proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - NiceMock visitor; - auto config_or_error = factory.createRouteSpecificFilterConfig( - proto_config, context_.server_factory_context_, visitor); - EXPECT_FALSE(config_or_error.ok()); - EXPECT_THAT(config_or_error.status().message(), - testing::HasSubstr("Failed to create pre-route filter config")); - - TestEnvironment::unsetEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH"); -} - -TEST_F(DynamicModuleFilterConfigTest, RouteSpecificConfigInvalidModule) { - const std::string yaml = R"EOF( - dynamic_module_config: - name: "nonexistent_module" - do_not_close: true - per_route_config_name: "test" - )EOF"; - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilterPerRoute proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - DynamicModuleConfigFactory factory; - NiceMock visitor; - auto config_or_error = factory.createRouteSpecificFilterConfig( - proto_config, context_.server_factory_context_, visitor); - EXPECT_FALSE(config_or_error.ok()); - EXPECT_THAT(config_or_error.status().message(), - testing::HasSubstr("Failed to load dynamic module")); -} - -// Verify that a successful fetch with invalid (non-.so) module bytes fails gracefully. -TEST_F(DynamicModuleFilterConfigTest, RemoteLoadingInvalidModuleBytes) { - const std::string invalid_bytes = "this is not a valid shared object binary"; - - const std::string sha256 = Hex::encode( - Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(invalid_bytes))); - - const std::string yaml = absl::StrCat(R"EOF( - dynamic_module_config: - module: - remote: - http_uri: - uri: https://example.com/module.so - cluster: cluster_1 - timeout: 5s - retry_policy: - num_retries: 0 - sha256: )EOF", - sha256, R"EOF( - do_not_close: true - filter_name: "test_filter" - )EOF"); - - envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); - - NiceMock client; - NiceMock request(&client); - - cluster_manager_.initializeThreadLocalClusters({"cluster_1"}); - EXPECT_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) - .WillOnce(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); - EXPECT_CALL(cluster_manager_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillOnce(testing::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().add(invalid_bytes); - callbacks.onSuccess(request, std::move(response)); - return &request; - })); - DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); - EXPECT_TRUE(cb_or_error.ok()) << cb_or_error.status().message(); - - EXPECT_CALL(init_watcher_, ready()); - init_manager_.initialize(init_watcher_); - EXPECT_EQ(init_manager_.state(), Init::Manager::State::Initialized); - - // Fetch succeeded but dlopen fails on invalid bytes, so filter_config stays null. - NiceMock filter_callback; - EXPECT_CALL(filter_callback, addStreamFilter(_)).Times(0); - cb_or_error.value()(filter_callback); + EXPECT_FALSE(cb_or_error.ok()); + EXPECT_THAT(cb_or_error.status().message(), testing::HasSubstr("Failed to load dynamic module")); } // Verify that when both name and module are set, module takes precedence. From 798b88705aaab8a3086372929325d933cbb84644 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Wed, 4 Mar 2026 15:42:56 +0530 Subject: [PATCH 20/21] move the test files Signed-off-by: Anurag Aggarwal --- test/extensions/dynamic_modules/http/BUILD | 19 +++++++++++++ .../http}/config_test.cc | 0 .../dynamic_modules/test_data/c/BUILD | 1 - .../filters/http/dynamic_modules/BUILD | 28 ------------------- 4 files changed, 19 insertions(+), 29 deletions(-) rename test/extensions/{filters/http/dynamic_modules => dynamic_modules/http}/config_test.cc (100%) delete mode 100644 test/extensions/filters/http/dynamic_modules/BUILD diff --git a/test/extensions/dynamic_modules/http/BUILD b/test/extensions/dynamic_modules/http/BUILD index 81e82e97abe74..b1bf3528e8eff 100644 --- a/test/extensions/dynamic_modules/http/BUILD +++ b/test/extensions/dynamic_modules/http/BUILD @@ -8,6 +8,25 @@ licenses(["notice"]) # Apache 2 envoy_package() +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/c:no_op", + ], + deps = [ + "//source/common/stats:isolated_store_lib", + "//source/extensions/filters/http/dynamic_modules:factory_lib", + "//test/extensions/dynamic_modules:util", + "//test/mocks/network:network_mocks", + "//test/mocks/server:server_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "factory_test", srcs = ["factory_test.cc"], diff --git a/test/extensions/filters/http/dynamic_modules/config_test.cc b/test/extensions/dynamic_modules/http/config_test.cc similarity index 100% rename from test/extensions/filters/http/dynamic_modules/config_test.cc rename to test/extensions/dynamic_modules/http/config_test.cc diff --git a/test/extensions/dynamic_modules/test_data/c/BUILD b/test/extensions/dynamic_modules/test_data/c/BUILD index c3ebb552aa12c..4777d3a7561eb 100644 --- a/test/extensions/dynamic_modules/test_data/c/BUILD +++ b/test/extensions/dynamic_modules/test_data/c/BUILD @@ -11,7 +11,6 @@ package(default_visibility = [ "//test/extensions/dynamic_modules/listener:__pkg__", "//test/extensions/dynamic_modules/network:__pkg__", "//test/extensions/dynamic_modules/udp:__pkg__", - "//test/extensions/filters/http/dynamic_modules:__pkg__", "//test/extensions/load_balancing_policies/dynamic_modules:__pkg__", "//test/extensions/matching/input_matchers/dynamic_modules:__pkg__", "//test/extensions/transport_sockets/tls/cert_validator/dynamic_modules:__pkg__", diff --git a/test/extensions/filters/http/dynamic_modules/BUILD b/test/extensions/filters/http/dynamic_modules/BUILD deleted file mode 100644 index 7c35a6da2e533..0000000000000 --- a/test/extensions/filters/http/dynamic_modules/BUILD +++ /dev/null @@ -1,28 +0,0 @@ -load( - "//bazel:envoy_build_system.bzl", - "envoy_cc_test", - "envoy_package", -) - -licenses(["notice"]) # Apache 2 - -envoy_package() - -envoy_cc_test( - name = "config_test", - srcs = ["config_test.cc"], - data = [ - "//test/extensions/dynamic_modules/test_data/c:no_op", - ], - deps = [ - "//source/common/stats:isolated_store_lib", - "//source/extensions/filters/http/dynamic_modules:factory_lib", - "//test/extensions/dynamic_modules:util", - "//test/mocks/network:network_mocks", - "//test/mocks/server:server_mocks", - "//test/test_common:environment_lib", - "//test/test_common:simulated_time_system_lib", - "//test/test_common:utility_lib", - "@envoy_api//envoy/extensions/filters/http/dynamic_modules/v3:pkg_cc_proto", - ], -) From b611d903081ca62b695ef5d3d5c345f829f93c19 Mon Sep 17 00:00:00 2001 From: Anurag Aggarwal Date: Wed, 4 Mar 2026 15:49:35 +0530 Subject: [PATCH 21/21] use loadFromYamlAndValidate Signed-off-by: Anurag Aggarwal --- test/extensions/dynamic_modules/http/config_test.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/extensions/dynamic_modules/http/config_test.cc b/test/extensions/dynamic_modules/http/config_test.cc index ff22b5162b53e..7eb9bbe3508cd 100644 --- a/test/extensions/dynamic_modules/http/config_test.cc +++ b/test/extensions/dynamic_modules/http/config_test.cc @@ -54,7 +54,7 @@ TEST_F(DynamicModuleFilterConfigTest, LocalFileLoading) { )EOF")); envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); + TestUtility::loadFromYamlAndValidate(yaml, proto_config); DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); @@ -76,7 +76,7 @@ TEST_F(DynamicModuleFilterConfigTest, InlineBytesRejected) { )EOF"; envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); + TestUtility::loadFromYamlAndValidate(yaml, proto_config); DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); @@ -93,7 +93,7 @@ TEST_F(DynamicModuleFilterConfigTest, NoModuleOrName) { )EOF"; envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); + TestUtility::loadFromYamlAndValidate(yaml, proto_config); DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); @@ -117,7 +117,7 @@ TEST_F(DynamicModuleFilterConfigTest, RemoteSourceRejected) { )EOF"; envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); + TestUtility::loadFromYamlAndValidate(yaml, proto_config); DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); @@ -137,7 +137,7 @@ TEST_F(DynamicModuleFilterConfigTest, InvalidLocalFile) { )EOF"; envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); + TestUtility::loadFromYamlAndValidate(yaml, proto_config); DynamicModuleConfigFactory factory; auto cb_or_error = factory.createFilterFactoryFromProto(proto_config, "stats", context_); @@ -161,7 +161,7 @@ TEST_F(DynamicModuleFilterConfigTest, ModulePrecedenceOverName) { )EOF")); envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config; - TestUtility::loadFromYaml(yaml, proto_config); + TestUtility::loadFromYamlAndValidate(yaml, proto_config); DynamicModuleConfigFactory factory; // If name were used, this would fail because "nonexistent_module_should_be_ignored" doesn't