diff --git a/CODEOWNERS b/CODEOWNERS index 02cf03558979..dd0f36e455fe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -333,6 +333,9 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123 /*/extensions/health_checkers/common @zuercher @botengyao +# Lua cluster specifier +/*/extensions/router/cluster_specifiers/lua @StarryVae @wbpcode + # Intentionally exempt (treated as core code) /*/extensions/filters/common @UNOWNED @UNOWNED /*/extensions/filters/http/common @UNOWNED @UNOWNED diff --git a/api/BUILD b/api/BUILD index e9eb8cd0c252..16a0037cf8e8 100644 --- a/api/BUILD +++ b/api/BUILD @@ -309,6 +309,7 @@ proto_library( "//envoy/extensions/retry/host/omit_host_metadata/v3:pkg", "//envoy/extensions/retry/host/previous_hosts/v3:pkg", "//envoy/extensions/retry/priority/previous_priorities/v3:pkg", + "//envoy/extensions/router/cluster_specifiers/lua/v3:pkg", "//envoy/extensions/stat_sinks/graphite_statsd/v3:pkg", "//envoy/extensions/stat_sinks/open_telemetry/v3:pkg", "//envoy/extensions/stat_sinks/wasm/v3:pkg", diff --git a/api/envoy/extensions/router/cluster_specifiers/lua/v3/BUILD b/api/envoy/extensions/router/cluster_specifiers/lua/v3/BUILD new file mode 100644 index 000000000000..09a37ad16b83 --- /dev/null +++ b/api/envoy/extensions/router/cluster_specifiers/lua/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/router/cluster_specifiers/lua/v3/lua.proto b/api/envoy/extensions/router/cluster_specifiers/lua/v3/lua.proto new file mode 100644 index 000000000000..b8ea10a02df7 --- /dev/null +++ b/api/envoy/extensions/router/cluster_specifiers/lua/v3/lua.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package envoy.extensions.router.cluster_specifiers.lua.v3; + +import "envoy/config/core/v3/base.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.router.cluster_specifiers.lua.v3"; +option java_outer_classname = "LuaProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/router/cluster_specifiers/lua/v3;luav3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Lua] +// +// Lua cluster specifier :ref:`configuration reference documentation `. +// [#extension: envoy.router.cluster_specifier_plugin.lua] + +message LuaConfig { + // The lua code that Envoy will execute to select cluster. + config.core.v3.DataSource source_code = 1 [(validate.rules).message = {required: true}]; + + // Default cluster. It will be used when the lua code execute failure. + string default_cluster = 2; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 828efeaa0dd2..65ba4dc5c75f 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -248,6 +248,7 @@ proto_library( "//envoy/extensions/retry/host/omit_host_metadata/v3:pkg", "//envoy/extensions/retry/host/previous_hosts/v3:pkg", "//envoy/extensions/retry/priority/previous_priorities/v3:pkg", + "//envoy/extensions/router/cluster_specifiers/lua/v3:pkg", "//envoy/extensions/stat_sinks/graphite_statsd/v3:pkg", "//envoy/extensions/stat_sinks/open_telemetry/v3:pkg", "//envoy/extensions/stat_sinks/wasm/v3:pkg", diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 720d74da85b5..b2317039cee5 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -477,7 +477,10 @@ REPOSITORY_LOCATIONS_SPEC = dict( urls = ["https://github.com/LuaJIT/LuaJIT/archive/{version}.tar.gz"], release_date = "2023-04-16", use_category = ["dataplane_ext"], - extensions = ["envoy.filters.http.lua"], + extensions = [ + "envoy.filters.http.lua", + "envoy.router.cluster_specifier_plugin.lua", + ], cpe = "cpe:2.3:a:luajit:luajit:*", license = "MIT", license_url = "https://github.com/LuaJIT/LuaJIT/blob/{version}/COPYRIGHT", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 5494ba2ba005..86c12032c43b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -320,6 +320,9 @@ new_features: to add multiple namespaces with one filter and config to overwrite existing metadata is opt-in. :ref:`untyped_metadata ` may now be used to configure the ``set_metadata`` filter. +- area: lua + change: | + Added lua extension of router cluster specifier plugin to support selecting cluster dynamically by lua code. deprecated: - area: wasm diff --git a/docs/root/api-v3/config/cluster_specifier/cluster_specifier.rst b/docs/root/api-v3/config/cluster_specifier/cluster_specifier.rst new file mode 100644 index 000000000000..6202887d3121 --- /dev/null +++ b/docs/root/api-v3/config/cluster_specifier/cluster_specifier.rst @@ -0,0 +1,8 @@ +Cluster specifier +================= + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../extensions/router/cluster_specifiers/*/v3/* diff --git a/docs/root/api-v3/config/config.rst b/docs/root/api-v3/config/config.rst index 015f5e20cb50..12d216da67c8 100644 --- a/docs/root/api-v3/config/config.rst +++ b/docs/root/api-v3/config/config.rst @@ -45,3 +45,4 @@ Extensions wasm/wasm watchdog/watchdog load_balancing_policies/load_balancing_policies + cluster_specifier/cluster_specifier diff --git a/docs/root/configuration/http/cluster_specifier/cluster_specifier.rst b/docs/root/configuration/http/cluster_specifier/cluster_specifier.rst index 54e3d14fc1ef..c3c799abf131 100644 --- a/docs/root/configuration/http/cluster_specifier/cluster_specifier.rst +++ b/docs/root/configuration/http/cluster_specifier/cluster_specifier.rst @@ -7,3 +7,4 @@ HTTP cluster specifier :maxdepth: 2 golang + lua diff --git a/docs/root/configuration/http/cluster_specifier/lua.rst b/docs/root/configuration/http/cluster_specifier/lua.rst new file mode 100644 index 000000000000..0cab42c25cd0 --- /dev/null +++ b/docs/root/configuration/http/cluster_specifier/lua.rst @@ -0,0 +1,92 @@ +.. _config_http_cluster_specifier_lua: + +Lua cluster specifier +===================== + +Overview +-------- + +The HTTP Lua cluster specifier allows `Lua `_ scripts to select router cluster +during the request flows. `LuaJIT `_ is used as the runtime. Because of this, the +supported Lua version is mostly 5.1 with some 5.2 features. See the `LuaJIT documentation +`_ for more details. + +Configuration +------------- + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.router.cluster_specifiers.lua.v3.LuaConfig``. +* :ref:`v3 API reference ` + +A simple example of configuring Lua cluster specifier is as follow: + +.. code-block:: yaml + + routes: + - match: + prefix: "/" + route: + inline_cluster_specifier_plugin: + extension: + name: envoy.router.cluster_specifier_plugin.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.router.cluster_specifiers.lua.v3.LuaConfig + source_code: + inline_string: | + function envoy_on_route(route_handle) + local header_value = route_handle:headers():get("header_key") + if header_value == "fake" then + return "fake_service" + end + return "web_service" + end + default_cluster: web_service + +Lua script defined in ``source_code`` will be executed to select router cluster, and just as cluster specifier +plugin in C++, Lua script can also select router cluster based on request headers. If Lua script execute failure, +``default_cluster`` will be used. + +Complete example +---------------- + +A complete example using Docker is available in :repo:`/examples/lua-cluster-specifier`. + +Route handle API +---------------- + +When Envoy loads the script in the configuration, it looks for a global function that the script defines: + +.. code-block:: lua + + function envoy_on_route(route_handle) + end + +During the route path, Envoy will run *envoy_on_route* as a coroutine, passing a handle to the route API. + +The following methods on the stream handle are supported: + +headers() +^^^^^^^^^ + +.. code-block:: lua + + local headers = handle:headers() + +Returns the stream's headers. The headers can be used to match to select a specific cluster. + +Returns a :ref:`header object `. + +.. _config_lua_cluster_specifier_header_wrapper: + +Header object API +----------------- + +get() +^^^^^ + +.. code-block:: lua + + headers:get(key) + +Gets a header. *key* is a string that supplies the header key. Returns a string that is the header +value or nil if there is no such header. If there are multiple headers in the same case-insensitive +key, their values will be combined with a *,* separator and returned as a string. diff --git a/docs/root/start/sandboxes/index.rst b/docs/root/start/sandboxes/index.rst index d04bf9eb78e7..aa8ce6e7ca85 100644 --- a/docs/root/start/sandboxes/index.rst +++ b/docs/root/start/sandboxes/index.rst @@ -65,6 +65,7 @@ The following sandboxes are available: load_reporting_service locality_load_balancing local_ratelimit + lua-cluster-specifier lua mysql opentelemetry diff --git a/docs/root/start/sandboxes/lua-cluster-specifier.rst b/docs/root/start/sandboxes/lua-cluster-specifier.rst new file mode 100644 index 000000000000..a7e4c852c849 --- /dev/null +++ b/docs/root/start/sandboxes/lua-cluster-specifier.rst @@ -0,0 +1,71 @@ +.. _install_sandboxes_lua_cluster_specifier: + +Lua Cluster Specifier +===================== + +.. sidebar:: Requirements + + .. include:: _include/docker-env-setup-link.rst + + :ref:`curl ` + Used to make ``HTTP`` requests. + +In this example, we show how the `Lua `_ cluster specifier can be used with the +Envoy proxy. + +The example Envoy proxy configuration includes a Lua cluster specifier plugin that contains a function: + +- ``envoy_on_route(route_handle)`` + +:ref:`See here ` for an overview of Envoy's Lua cluster specifier +and documentation regarding the function. + +Step 1: Build the sandbox +************************* + +Change to the ``examples/lua-cluster-specifier`` directory. + +.. code-block:: console + + $ pwd + envoy/examples/lua-cluster-specifier + $ docker compose pull + $ docker compose up --build -d + $ docker compose ps + + Name Command State Ports + -------------------------------------------------------------------------------------------- + lua-cluster-specifier-proxy-1 /docker-entrypoint.sh /usr ... Up 10000/tcp, 0.0.0.0:8000->8000/tcp + lua-cluster-specifier-web_service-1 /bin/echo-server Up 0.0.0.0:8080->8080/tcp + +Step 2: Send a request to the normal service +******************************************** + +The output from the ``curl`` command below should return 200, since the lua code select the normal service. + +Terminal 1 + +.. code-block:: console + + $ curl -i localhost:8000/anything 2>&1 |grep 200 + HTTP/1.1 200 OK + +Step 3: Send a request to the fake service +****************************************** + +The output from the ``curl`` command below should return 503, since the lua code select the fake service. + +Terminal 1 + +.. code-block:: console + + $ curl -i localhost:8000/anything -H "header_key:fake" 2>&1 |grep 503 + HTTP/1.1 503 Service Unavailable + +.. seealso:: + + :ref:`Envoy Lua cluster specifier ` + Learn more about the Envoy Lua cluster specifier. + + `Lua `_ + The Lua programming language. diff --git a/examples/lua-cluster-specifier/README.md b/examples/lua-cluster-specifier/README.md new file mode 100644 index 000000000000..0a1cd344b9b1 --- /dev/null +++ b/examples/lua-cluster-specifier/README.md @@ -0,0 +1,2 @@ +To learn about this sandbox and for instructions on how to run it please head over +to the [envoy docs](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/lua-cluster-specifier.html) diff --git a/examples/lua-cluster-specifier/docker-compose.yaml b/examples/lua-cluster-specifier/docker-compose.yaml new file mode 100644 index 000000000000..904b030f8417 --- /dev/null +++ b/examples/lua-cluster-specifier/docker-compose.yaml @@ -0,0 +1,14 @@ +services: + + proxy: + build: + context: . + dockerfile: ../shared/envoy/Dockerfile + ports: + - "${PORT_PROXY:-8000}:8000" + + web_service: + build: + context: ../shared/echo + ports: + - "${PORT_WEB:-8080}:8080" diff --git a/examples/lua-cluster-specifier/envoy.yaml b/examples/lua-cluster-specifier/envoy.yaml new file mode 100644 index 000000000000..058e28179395 --- /dev/null +++ b/examples/lua-cluster-specifier/envoy.yaml @@ -0,0 +1,58 @@ +static_resources: + listeners: + - name: main + address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + inline_cluster_specifier_plugin: + extension: + name: envoy.router.cluster_specifier_plugin.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.router.cluster_specifiers.lua.v3.LuaConfig + source_code: + inline_string: | + function envoy_on_route(route_handle) + local header_value = route_handle:headers():get("header_key") + if header_value == "fake" then + return "fake_service" + end + return "web_service" + end + default_cluster: web_service + + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: web_service + type: STRICT_DNS # static + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: web_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin.org + port_value: 80 diff --git a/examples/lua-cluster-specifier/verify.sh b/examples/lua-cluster-specifier/verify.sh new file mode 100755 index 000000000000..ec37d5b46d89 --- /dev/null +++ b/examples/lua-cluster-specifier/verify.sh @@ -0,0 +1,19 @@ +#!/bin/bash -e + +export NAME=lua-cluster-specifier +export PORT_PROXY="${LUA_CLUSTER_PORT_PROXY:-12620}" +export PORT_WEB="${LUA_CLUSTER_PORT_WEB:-12621}" + +# shellcheck source=examples/verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + +run_log "Test Lua cluster specifier with normal cluster" +responds_with_header \ + "HTTP/1.1 200 OK" \ + "http://localhost:${PORT_PROXY}/" + +run_log "Test Lua cluster specifier with fake cluster" +responds_with_header \ + "HTTP/1.1 503 Service Unavailable" \ + "http://localhost:${PORT_PROXY}/" \ + -H 'header_key: fake' diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index efa1de486e61..a67bf3be4744 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -494,6 +494,12 @@ EXTENSIONS = { # Geolocation Provider # "envoy.geoip_providers.maxmind": "//source/extensions/geoip_providers/maxmind:config", + + # + # cluster specifier plugin + # + + "envoy.router.cluster_specifier_plugin.lua": "//source/extensions/router/cluster_specifiers/lua:config", } # These can be changed to ["//visibility:public"], for downstream builds which diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 739f8a5003d3..587fb00c0363 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1073,6 +1073,13 @@ envoy.route.early_data_policy.default: status: stable type_urls: - envoy.extensions.early_data.v3.DefaultEarlyDataPolicy +envoy.router.cluster_specifier_plugin.lua: + categories: + - envoy.router.cluster_specifier_plugin + security_posture: robust_to_untrusted_downstream_and_upstream + status: alpha + type_urls: + - envoy.extensions.router.cluster_specifiers.lua.v3.LuaConfig envoy.stat_sinks.dog_statsd: categories: - envoy.stats_sinks diff --git a/source/extensions/router/cluster_specifiers/lua/BUILD b/source/extensions/router/cluster_specifiers/lua/BUILD new file mode 100644 index 000000000000..92927266d15e --- /dev/null +++ b/source/extensions/router/cluster_specifiers/lua/BUILD @@ -0,0 +1,41 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +# Lua cluster specifier plugin. + +envoy_extension_package() + +envoy_cc_library( + name = "lua_cluster_specifier_lib", + srcs = [ + "lua_cluster_specifier.cc", + ], + hdrs = [ + "lua_cluster_specifier.h", + ], + deps = [ + "//envoy/router:cluster_specifier_plugin_interface", + "//source/common/common:utility_lib", + "//source/common/http:utility_lib", + "//source/common/router:config_lib", + "//source/extensions/filters/common/lua:lua_lib", + "//source/extensions/filters/common/lua:wrappers_lib", + "@envoy_api//envoy/extensions/router/cluster_specifiers/lua/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":lua_cluster_specifier_lib", + "//envoy/registry", + ], +) diff --git a/source/extensions/router/cluster_specifiers/lua/config.cc b/source/extensions/router/cluster_specifiers/lua/config.cc new file mode 100644 index 000000000000..fb7930caf859 --- /dev/null +++ b/source/extensions/router/cluster_specifiers/lua/config.cc @@ -0,0 +1,23 @@ +#include "source/extensions/router/cluster_specifiers/lua/config.h" + +namespace Envoy { +namespace Extensions { +namespace Router { +namespace Lua { + +Envoy::Router::ClusterSpecifierPluginSharedPtr +LuaClusterSpecifierPluginFactoryConfig::createClusterSpecifierPlugin( + const Protobuf::Message& config, Server::Configuration::CommonFactoryContext& context) { + + const auto& typed_config = dynamic_cast(config); + auto cluster_config = std::make_shared(typed_config, context); + return std::make_shared(cluster_config); +} + +REGISTER_FACTORY(LuaClusterSpecifierPluginFactoryConfig, + Envoy::Router::ClusterSpecifierPluginFactoryConfig); + +} // namespace Lua +} // namespace Router +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/router/cluster_specifiers/lua/config.h b/source/extensions/router/cluster_specifiers/lua/config.h new file mode 100644 index 000000000000..2b9793b08099 --- /dev/null +++ b/source/extensions/router/cluster_specifiers/lua/config.h @@ -0,0 +1,28 @@ +#pragma once + +#include "source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.h" + +namespace Envoy { +namespace Extensions { +namespace Router { +namespace Lua { + +class LuaClusterSpecifierPluginFactoryConfig + : public Envoy::Router::ClusterSpecifierPluginFactoryConfig { +public: + LuaClusterSpecifierPluginFactoryConfig() = default; + Envoy::Router::ClusterSpecifierPluginSharedPtr + createClusterSpecifierPlugin(const Protobuf::Message& config, + Server::Configuration::CommonFactoryContext&) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { return "envoy.router.cluster_specifier_plugin.lua"; } +}; + +} // namespace Lua +} // namespace Router +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.cc b/source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.cc new file mode 100644 index 000000000000..c226a78c4a0a --- /dev/null +++ b/source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.cc @@ -0,0 +1,93 @@ +#include "source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.h" + +#include "source/common/router/config_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Router { +namespace Lua { + +PerLuaCodeSetup::PerLuaCodeSetup(const std::string& lua_code, ThreadLocal::SlotAllocator& tls) + : lua_state_(lua_code, tls) { + lua_state_.registerType(); + lua_state_.registerType(); + + const Filters::Common::Lua::InitializerList initializers; + + cluster_function_slot_ = lua_state_.registerGlobal("envoy_on_route", initializers); + if (lua_state_.getGlobalRef(cluster_function_slot_) == LUA_REFNIL) { + throw EnvoyException( + "envoy_on_route() function not found. Lua will not hook cluster specifier."); + } +} + +int HeaderMapWrapper::luaGet(lua_State* state) { + absl::string_view key = Filters::Common::Lua::getStringViewFromLuaString(state, 2); + const Envoy::Http::HeaderUtility::GetAllOfHeaderAsStringResult value = + Envoy::Http::HeaderUtility::getAllOfHeaderAsString(headers_, + Envoy::Http::LowerCaseString(key)); + if (value.result().has_value()) { + lua_pushlstring(state, value.result().value().data(), value.result().value().size()); + return 1; + } else { + return 0; + } +} + +int RouteHandleWrapper::luaHeaders(lua_State* state) { + if (headers_wrapper_.get() != nullptr) { + headers_wrapper_.pushStack(); + } else { + headers_wrapper_.reset(HeaderMapWrapper::create(state, headers_), true); + } + return 1; +} + +LuaClusterSpecifierConfig::LuaClusterSpecifierConfig( + const LuaClusterSpecifierConfigProto& config, + Server::Configuration::CommonFactoryContext& context) + : main_thread_dispatcher_(context.mainThreadDispatcher()), + default_cluster_(config.default_cluster()) { + const std::string code_str = Config::DataSource::read(config.source_code(), true, context.api()); + per_lua_code_setup_ptr_ = std::make_unique(code_str, context.threadLocal()); +} + +LuaClusterSpecifierPlugin::LuaClusterSpecifierPlugin(LuaClusterSpecifierConfigSharedPtr config) + : config_(config), + function_ref_(config_->perLuaCodeSetup() ? config_->perLuaCodeSetup()->clusterFunctionRef() + : LUA_REFNIL) {} + +std::string LuaClusterSpecifierPlugin::startLua(const Http::HeaderMap& headers) const { + if (function_ref_ == LUA_REFNIL) { + return config_->defaultCluster(); + } + Filters::Common::Lua::CoroutinePtr coroutine = config_->perLuaCodeSetup()->createCoroutine(); + + RouteHandleRef handle; + handle.reset(RouteHandleWrapper::create(coroutine->luaState(), headers), true); + + TRY_NEEDS_AUDIT { + coroutine->start(function_ref_, 1, []() {}); + } + END_TRY catch (const Filters::Common::Lua::LuaException& e) { + ENVOY_LOG(error, "script log: {}, use default cluster", e.what()); + return config_->defaultCluster(); + } + if (!lua_isstring(coroutine->luaState(), -1)) { + ENVOY_LOG(error, "script log: return value is not string, use default cluster"); + return config_->defaultCluster(); + } + return std::string(Filters::Common::Lua::getStringViewFromLuaString(coroutine->luaState(), -1)); +} + +Envoy::Router::RouteConstSharedPtr +LuaClusterSpecifierPlugin::route(Envoy::Router::RouteConstSharedPtr parent, + const Http::RequestHeaderMap& headers) const { + return std::make_shared( + dynamic_cast(parent.get()), parent, + startLua(headers)); +} +} // namespace Lua +} // namespace Router +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.h b/source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.h new file mode 100644 index 000000000000..5a6ddb9d6a08 --- /dev/null +++ b/source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.h @@ -0,0 +1,119 @@ +#pragma once + +#include "envoy/extensions/router/cluster_specifiers/lua/v3/lua.pb.h" +#include "envoy/router/cluster_specifier_plugin.h" + +#include "source/common/config/datasource.h" +#include "source/extensions/filters/common/lua/wrappers.h" + +namespace Envoy { +namespace Extensions { +namespace Router { +namespace Lua { + +using LuaClusterSpecifierConfigProto = + envoy::extensions::router::cluster_specifiers::lua::v3::LuaConfig; + +class PerLuaCodeSetup : Logger::Loggable { +public: + PerLuaCodeSetup(const std::string& lua_code, ThreadLocal::SlotAllocator& tls); + + Extensions::Filters::Common::Lua::CoroutinePtr createCoroutine() { + return lua_state_.createCoroutine(); + } + + int clusterFunctionRef() { return lua_state_.getGlobalRef(cluster_function_slot_); } + +private: + uint64_t cluster_function_slot_{}; + + Filters::Common::Lua::ThreadLocalState lua_state_; +}; + +using PerLuaCodeSetupPtr = std::unique_ptr; + +class HeaderMapWrapper : public Filters::Common::Lua::BaseLuaObject { +public: + HeaderMapWrapper(const Http::HeaderMap& headers) : headers_(headers) {} + + static ExportedFunctions exportedFunctions() { return {{"get", static_luaGet}}; } + +private: + /** + * Get a header value from the map. + * @param 1 (string): header name. + * @return string value if found or nil. + */ + DECLARE_LUA_FUNCTION(HeaderMapWrapper, luaGet); + + const Http::HeaderMap& headers_; +}; + +using HeaderMapRef = Filters::Common::Lua::LuaDeathRef; + +class RouteHandleWrapper : public Filters::Common::Lua::BaseLuaObject { +public: + RouteHandleWrapper(const Http::HeaderMap& headers) : headers_(headers) {} + + static ExportedFunctions exportedFunctions() { return {{"headers", static_luaHeaders}}; } + +private: + /** + * @return a handle to the headers. + */ + DECLARE_LUA_FUNCTION(RouteHandleWrapper, luaHeaders); + + const Http::HeaderMap& headers_; + HeaderMapRef headers_wrapper_; +}; + +using RouteHandleRef = Filters::Common::Lua::LuaDeathRef; + +class LuaClusterSpecifierConfig : Logger::Loggable { +public: + LuaClusterSpecifierConfig(const LuaClusterSpecifierConfigProto& config, + Server::Configuration::CommonFactoryContext& context); + + ~LuaClusterSpecifierConfig() { + // The design of the TLS system does not allow TLS state to be modified in worker threads. + // However, when the route configuration is dynamically updated via RDS, the old + // LuaClusterSpecifierConfig object may be destructed in a random worker thread. Therefore, to + // ensure thread safety, ownership of per_lua_code_setup_ptr_ must be transferred to the main + // thread and destroyed when the LuaClusterSpecifierConfig object is not destructed in the main + // thread. + if (per_lua_code_setup_ptr_ && !main_thread_dispatcher_.isThreadSafe()) { + auto shared_ptr_wrapper = + std::make_shared(std::move(per_lua_code_setup_ptr_)); + main_thread_dispatcher_.post([shared_ptr_wrapper] { shared_ptr_wrapper->reset(); }); + } + } + + PerLuaCodeSetup* perLuaCodeSetup() const { return per_lua_code_setup_ptr_.get(); } + const std::string& defaultCluster() const { return default_cluster_; } + +private: + Event::Dispatcher& main_thread_dispatcher_; + PerLuaCodeSetupPtr per_lua_code_setup_ptr_; + const std::string default_cluster_; +}; + +using LuaClusterSpecifierConfigSharedPtr = std::shared_ptr; + +class LuaClusterSpecifierPlugin : public Envoy::Router::ClusterSpecifierPlugin, + Logger::Loggable { +public: + LuaClusterSpecifierPlugin(LuaClusterSpecifierConfigSharedPtr config); + Envoy::Router::RouteConstSharedPtr route(Envoy::Router::RouteConstSharedPtr parent, + const Http::RequestHeaderMap& header) const override; + +private: + std::string startLua(const Http::HeaderMap& headers) const; + + LuaClusterSpecifierConfigSharedPtr config_; + const int function_ref_; +}; + +} // namespace Lua +} // namespace Router +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/router/cluster_specifiers/lua/BUILD b/test/extensions/router/cluster_specifiers/lua/BUILD new file mode 100644 index 000000000000..f086f868f56c --- /dev/null +++ b/test/extensions/router/cluster_specifiers/lua/BUILD @@ -0,0 +1,35 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "lua_cluster_specifier_test", + srcs = ["lua_cluster_specifier_test.cc"], + extension_names = ["envoy.router.cluster_specifier_plugin.lua"], + deps = [ + "//source/extensions/router/cluster_specifiers/lua:lua_cluster_specifier_lib", + "//test/mocks/router:router_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_names = ["envoy.router.cluster_specifier_plugin.lua"], + deps = [ + "//source/extensions/router/cluster_specifiers/lua:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/router/cluster_specifiers/lua/config_test.cc b/test/extensions/router/cluster_specifiers/lua/config_test.cc new file mode 100644 index 000000000000..ff23f0a556b6 --- /dev/null +++ b/test/extensions/router/cluster_specifiers/lua/config_test.cc @@ -0,0 +1,69 @@ +#include "source/extensions/router/cluster_specifiers/lua/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Router { +namespace Lua { + +TEST(LuaClusterSpecifierPluginConfigTest, EmptyConfig) { + LuaClusterSpecifierPluginFactoryConfig factory; + + ProtobufTypes::MessagePtr empty_config = factory.createEmptyConfigProto(); + EXPECT_NE(nullptr, empty_config); +} + +TEST(LuaClusterSpecifierPluginConfigTest, NormalConfig) { + const std::string normal_lua_config_yaml = R"EOF( + source_code: + inline_string: | + function envoy_on_route(route_handle) + local header_value = route_handle:headers():get("header_key") + if header_value == "fake" then + return "fake_service" + end + return "web_service" + end + default_cluster: default_service + )EOF"; + + LuaClusterSpecifierConfigProto proto_config{}; + TestUtility::loadFromYaml(normal_lua_config_yaml, proto_config); + NiceMock context; + LuaClusterSpecifierPluginFactoryConfig factory; + Envoy::Router::ClusterSpecifierPluginSharedPtr plugin = + factory.createClusterSpecifierPlugin(proto_config, context); + EXPECT_NE(nullptr, plugin); +} + +TEST(LuaClusterSpecifierPluginConfigTest, NoOnRouteConfig) { + const std::string normal_lua_config_yaml = R"EOF( + source_code: + inline_string: | + function envoy_on_no_route(route_handle) + local header_value = route_handle:headers():get("header_key") + if header_value == "fake" then + return "fake_service" + end + return "web_service" + end + default_cluster: default_service + )EOF"; + + LuaClusterSpecifierConfigProto proto_config{}; + TestUtility::loadFromYaml(normal_lua_config_yaml, proto_config); + NiceMock context; + LuaClusterSpecifierPluginFactoryConfig factory; + EXPECT_THROW_WITH_MESSAGE( + factory.createClusterSpecifierPlugin(proto_config, context), Envoy::EnvoyException, + "envoy_on_route() function not found. Lua will not hook cluster specifier."); +} + +} // namespace Lua +} // namespace Router +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/router/cluster_specifiers/lua/lua_cluster_specifier_test.cc b/test/extensions/router/cluster_specifiers/lua/lua_cluster_specifier_test.cc new file mode 100644 index 000000000000..08e9480b769a --- /dev/null +++ b/test/extensions/router/cluster_specifiers/lua/lua_cluster_specifier_test.cc @@ -0,0 +1,144 @@ +#include "source/extensions/router/cluster_specifiers/lua/lua_cluster_specifier.h" + +#include "test/mocks/router/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Router { +namespace Lua { + +using testing::InSequence; +using testing::NiceMock; +using testing::Return; + +class LuaClusterSpecifierPluginTest : public testing::Test { +public: + void setUpTest(const std::string& yaml) { + LuaClusterSpecifierConfigProto proto_config{}; + TestUtility::loadFromYaml(yaml, proto_config); + + config_ = std::make_shared(proto_config, server_factory_context_); + + plugin_ = std::make_unique(config_); + } + + const std::string normal_lua_config_yaml_ = R"EOF( + source_code: + inline_string: | + function envoy_on_route(route_handle) + local header_value = route_handle:headers():get("header_key") + if header_value == "fake" then + return "fake_service" + end + return "web_service" + end + default_cluster: default_service + )EOF"; + + const std::string error_lua_config_yaml_ = R"EOF( + source_code: + inline_string: | + function envoy_on_route(route_handle) + local header_value = route_handle:headers():get({}) + if header_value == "fake" then + return "fake_service" + end + return "web_service" + end + default_cluster: default_service + )EOF"; + + const std::string return_type_not_string_lua_config_yaml_ = R"EOF( + source_code: + inline_string: | + function envoy_on_route(route_handle) + local header_value = route_handle:headers():get("header_key") + if header_value == "fake" then + return "fake_service" + end + return {} + end + default_cluster: default_service + )EOF"; + + NiceMock server_factory_context_; + std::unique_ptr plugin_; + LuaClusterSpecifierConfigSharedPtr config_; +}; + +// Normal lua code test. +TEST_F(LuaClusterSpecifierPluginTest, NormalLuaCode) { + setUpTest(normal_lua_config_yaml_); + + auto mock_route = std::make_shared>(); + { + Http::TestRequestHeaderMapImpl headers{{":path", "/"}, {"header_key", "fake"}}; + auto route = plugin_->route(mock_route, headers); + EXPECT_EQ("fake_service", route->routeEntry()->clusterName()); + } + + { + Http::TestRequestHeaderMapImpl headers{{":path", "/"}, {"header_key", "header_value"}}; + auto route = plugin_->route(mock_route, headers); + EXPECT_EQ("web_service", route->routeEntry()->clusterName()); + } +} + +// Error lua code test. +TEST_F(LuaClusterSpecifierPluginTest, ErrorLuaCode) { + setUpTest(error_lua_config_yaml_); + + auto mock_route = std::make_shared>(); + { + Http::TestRequestHeaderMapImpl headers{{":path", "/"}, {"header_key", "fake"}}; + auto route = plugin_->route(mock_route, headers); + EXPECT_EQ("default_service", route->routeEntry()->clusterName()); + } + + { + Http::TestRequestHeaderMapImpl headers{{":path", "/"}, {"header_key", "header_value"}}; + auto route = plugin_->route(mock_route, headers); + EXPECT_EQ("default_service", route->routeEntry()->clusterName()); + } +} + +// Return type not string lua code test. +TEST_F(LuaClusterSpecifierPluginTest, ReturnTypeNotStringLuaCode) { + setUpTest(return_type_not_string_lua_config_yaml_); + + auto mock_route = std::make_shared>(); + { + Http::TestRequestHeaderMapImpl headers{{":path", "/"}, {"header_key", "fake"}}; + auto route = plugin_->route(mock_route, headers); + EXPECT_EQ("fake_service", route->routeEntry()->clusterName()); + } + + { + Http::TestRequestHeaderMapImpl headers{{":path", "/"}, {"header_key", "header_value"}}; + auto route = plugin_->route(mock_route, headers); + EXPECT_EQ("default_service", route->routeEntry()->clusterName()); + } +} + +TEST_F(LuaClusterSpecifierPluginTest, DestructLuaClusterSpecifierConfig) { + setUpTest(normal_lua_config_yaml_); + InSequence s; + EXPECT_CALL(server_factory_context_.dispatcher_, isThreadSafe()).WillOnce(Return(false)); + EXPECT_CALL(server_factory_context_.dispatcher_, post(_)); + EXPECT_CALL(server_factory_context_.dispatcher_, isThreadSafe()).WillOnce(Return(true)); + EXPECT_CALL(server_factory_context_.dispatcher_, post(_)).Times(0); + + LuaClusterSpecifierConfigProto proto_config{}; + TestUtility::loadFromYaml(normal_lua_config_yaml_, proto_config); + config_ = std::make_shared(proto_config, server_factory_context_); + config_.reset(); +} + +} // namespace Lua +} // namespace Router +} // namespace Extensions +} // namespace Envoy