Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
74ae885
dynamic modules: add remote fetching for the module binary
kanurag94 Feb 3, 2026
7f80388
Merge remote-tracking branch 'upstream/main' into dym-loading
kanurag94 Feb 5, 2026
810558e
add tests and cleanup
kanurag94 Feb 5, 2026
ec4159f
attempt fixing coverage
kanurag94 Feb 5, 2026
93caee8
attempt fixing coverage
kanurag94 Feb 5, 2026
13d1089
lower coverage percentage for dynamic_modules filter
kanurag94 Feb 5, 2026
b708a8f
fix issues related to fetch
kanurag94 Feb 9, 2026
ed84cf4
Merge remote-tracking branch 'upstream/main' into dym-loading
kanurag94 Feb 9, 2026
d4a09af
Merge remote-tracking branch 'origin/dym-loading' into dym-loading
kanurag94 Feb 9, 2026
52c7e77
fix lint
kanurag94 Feb 9, 2026
ec9edb5
lower coverage due proto validation not invoking lines
kanurag94 Feb 9, 2026
5113c7e
add changelog
kanurag94 Feb 11, 2026
b1f5055
Merge remote-tracking branch 'upstream/main' into dym-loading
kanurag94 Mar 3, 2026
028fb4d
address review comments: remove cache related code
kanurag94 Mar 3, 2026
ce47d78
refactoring: logging and avoid buffer copy
kanurag94 Mar 3, 2026
0b63daa
review: remove inline source
kanurag94 Mar 3, 2026
b793679
refactor the code
kanurag94 Mar 3, 2026
29170e1
fix some failing tests
kanurag94 Mar 3, 2026
c186ec4
simpler refactoring
kanurag94 Mar 3, 2026
69b6956
Merge remote-tracking branch 'upstream/main' into dym-loading
kanurag94 Mar 3, 2026
19cb20a
reuse methods in factory
kanurag94 Mar 3, 2026
d134e6a
add exec permissions to temp file
kanurag94 Mar 3, 2026
c838d3b
add owner_all permissions to temp file
kanurag94 Mar 3, 2026
4568023
try keeping local.filename support only
kanurag94 Mar 4, 2026
798b887
move the test files
kanurag94 Mar 4, 2026
b611d90
use loadFromYamlAndValidate
kanurag94 Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion api/envoy/extensions/dynamic_modules/v3/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")
licenses(["notice"]) # Apache 2

api_proto_package(
deps = ["@xds//udpa/annotations:pkg"],
deps = [
"//envoy/config/core/v3:pkg",
"@xds//udpa/annotations:pkg",
],
)
16 changes: 13 additions & 3 deletions api/envoy/extensions/dynamic_modules/v3/dynamic_modules.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ syntax = "proto3";

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";
Expand All @@ -30,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: 6]
// [#next-free-field: 7]
message DynamicModuleConfig {
// The name of the dynamic module.
//
Expand All @@ -42,9 +43,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}];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this validation as we might want to deprecate this in future.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I think we won't deprecate it. But your change is correct.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we won't deprecate yeah because of #43696

string name = 1;

// If true, prevents the module from being unloaded with ``dlclose``.
//
Expand Down Expand Up @@ -80,4 +84,10 @@ message DynamicModuleConfig {
//
// Defaults to ``dynamicmodulescustom``.
string metrics_namespace = 5;

// 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;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I named it module instead of source as per the patterns we have in the existing code where a datasource signifies what it is fetching. Reading dynamic_module_config.module also seemed better than dynamic_module_config.source which might mean configSource and not moduleSource.

}
6 changes: 6 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ 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 file paths via the new
:ref:`module <envoy_v3_api_field_extensions.dynamic_modules.v3.DynamicModuleConfig.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
Expand Down
1 change: 1 addition & 0 deletions source/extensions/filters/http/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ envoy_cc_library(
":abi_impl",
":filter_config_lib",
":filter_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",
],
Expand Down
21 changes: 19 additions & 2 deletions source/extensions/filters/http/dynamic_modules/factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,25 @@ absl::StatusOr<Http::FilterFactoryCb> DynamicModuleConfigFactory::createFilterFa
Server::Configuration::ServerFactoryContext& context, Stats::Scope& scope) {

const auto& module_config = proto_config.dynamic_module_config();
auto dynamic_module = Extensions::DynamicModules::newDynamicModuleByName(
module_config.name(), module_config.do_not_close(), module_config.load_globally());

// Load the module: either from a local file path or by name.
absl::StatusOr<Extensions::DynamicModules::DynamicModulePtr> dynamic_module;
if (module_config.has_module()) {
if (!module_config.module().has_local() || !module_config.module().local().has_filename()) {
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(),
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: " +
std::string(dynamic_module.status().message()));
Expand Down
19 changes: 19 additions & 0 deletions test/extensions/dynamic_modules/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
175 changes: 175 additions & 0 deletions test/extensions/dynamic_modules/http/config_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#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/stats/isolated_store_impl.h"
#include "source/extensions/filters/http/dynamic_modules/factory.h"

#include "test/extensions/dynamic_modules/util.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::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_));
}

NiceMock<Network::MockListenerInfo> 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"};
Init::ExpectableWatcherImpl init_watcher_;

NiceMock<Server::Configuration::MockFactoryContext> context_;
};

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::loadFromYamlAndValidate(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, InlineBytesRejected) {
const std::string yaml = R"EOF(
dynamic_module_config:
module:
local:
inline_bytes: "AAAA"
do_not_close: true
filter_name: "test_filter"
)EOF";

envoy::extensions::filters::http::dynamic_modules::v3::DynamicModuleFilter proto_config;
TestUtility::loadFromYamlAndValidate(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("Only local file path is supported"));
}

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::loadFromYamlAndValidate(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, RemoteSourceRejected) {
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::loadFromYamlAndValidate(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("Only local file path is supported"));
}

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::loadFromYamlAndValidate(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"));
}

// 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::loadFromYamlAndValidate(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
Loading