diff --git a/.bazelrc b/.bazelrc index dae08913379f..53940f9526cb 100644 --- a/.bazelrc +++ b/.bazelrc @@ -109,7 +109,8 @@ build:libc++ --config=clang build:libc++ --action_env=CXXFLAGS=-stdlib=libc++ build:libc++ --action_env=LDFLAGS=-stdlib=libc++ build:libc++ --action_env=BAZEL_CXXOPTS=-stdlib=libc++ -build:libc++ --action_env=BAZEL_LINKLIBS=-l%:libc++.a:-l%:libc++abi.a:-lm +build:libc++ --action_env=BAZEL_LINKLIBS=-l%:libc++.a:-l%:libc++abi.a +build:libc++ --action_env=BAZEL_LINKOPTS=-lm:-pthread build:libc++ --define force_libcpp=enabled # Optimize build for binary size reduction. @@ -191,6 +192,8 @@ build:remote --spawn_strategy=remote,sandboxed,local build:remote --strategy=Javac=remote,sandboxed,local build:remote --strategy=Closure=remote,sandboxed,local build:remote --strategy=Genrule=remote,sandboxed,local +# rules_rust is not remote runnable (yet) +build:remote --strategy=Rustc=sandboxed,local build:remote --remote_timeout=7200 build:remote --auth_enabled=true build:remote --remote_download_toplevel diff --git a/CODEOWNERS b/CODEOWNERS index a8546ebd99de..c345892a9fb1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,6 +80,14 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/listener/http_inspector @yxue @PiotrSikora @lizan # attribute context /*/extensions/filters/common/expr @kyessenov @yangminzhu @lizan +# webassembly access logger extensions +/*/extensions/access_loggers/wasm @jplevyak @PiotrSikora @lizan +# webassembly bootstrap extensions +/*/extensions/bootstrap/wasm @jplevyak @PiotrSikora @lizan +# webassembly http extensions +/*/extensions/filters/http/wasm @jplevyak @PiotrSikora @lizan +# webassembly network extensions +/*/extensions/filters/network/wasm @jplevyak @PiotrSikora @lizan # webassembly common extension /*/extensions/common/wasm @jplevyak @PiotrSikora @lizan # common matcher @@ -107,6 +115,8 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/stat_sinks/dog_statsd @taiki45 @jmarantz /*/extensions/stat_sinks/hystrix @trabetti @jmarantz /*/extensions/stat_sinks/metrics_service @ramaraochavali @jmarantz +# webassembly stat-sink extensions +/*/extensions/stat_sinks/wasm @Aakash2017 @jplevyak @lizan /*/extensions/resource_monitors/injected_resource @eziskind @htuch /*/extensions/resource_monitors/common @eziskind @htuch /*/extensions/resource_monitors/fixed_heap @eziskind @htuch diff --git a/api/BUILD b/api/BUILD index b13a579dd18c..ed8743b793e3 100644 --- a/api/BUILD +++ b/api/BUILD @@ -237,6 +237,7 @@ proto_library( "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/retry/host/omit_host_metadata/v3:pkg", "//envoy/extensions/retry/priority/previous_priorities/v3:pkg", + "//envoy/extensions/stat_sinks/wasm/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", "//envoy/extensions/transport_sockets/proxy_protocol/v3:pkg", "//envoy/extensions/transport_sockets/quic/v3:pkg", diff --git a/api/envoy/extensions/access_loggers/wasm/v3/wasm.proto b/api/envoy/extensions/access_loggers/wasm/v3/wasm.proto index cd9db5906436..413743a203f0 100644 --- a/api/envoy/extensions/access_loggers/wasm/v3/wasm.proto +++ b/api/envoy/extensions/access_loggers/wasm/v3/wasm.proto @@ -12,9 +12,12 @@ option java_outer_classname = "WasmProto"; option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [[#not-implemented-hide:] +// [#protodoc-title: Wasm access log] +// [#extension: envoy.access_loggers.wasm] + // Custom configuration for an :ref:`AccessLog ` -// that calls into a WASM VM. +// that calls into a WASM VM. Configures the built-in *envoy.access_loggers.wasm* +// AccessLog. message WasmAccessLog { envoy.extensions.wasm.v3.PluginConfig config = 1; } diff --git a/api/envoy/extensions/filters/http/wasm/v3/wasm.proto b/api/envoy/extensions/filters/http/wasm/v3/wasm.proto index a812992a5b84..55eba141f45f 100644 --- a/api/envoy/extensions/filters/http/wasm/v3/wasm.proto +++ b/api/envoy/extensions/filters/http/wasm/v3/wasm.proto @@ -13,7 +13,10 @@ option java_outer_classname = "WasmProto"; option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [[#not-implemented-hide:] +// [#protodoc-title: Wasm] +// [#extension: envoy.filters.http.wasm] +// Wasm :ref:`configuration overview `. + message Wasm { // General Plugin configuration. envoy.extensions.wasm.v3.PluginConfig config = 1; diff --git a/api/envoy/extensions/filters/network/wasm/v3/wasm.proto b/api/envoy/extensions/filters/network/wasm/v3/wasm.proto index 131582762b59..0c1ac6af440e 100644 --- a/api/envoy/extensions/filters/network/wasm/v3/wasm.proto +++ b/api/envoy/extensions/filters/network/wasm/v3/wasm.proto @@ -13,7 +13,10 @@ option java_outer_classname = "WasmProto"; option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [[#not-implemented-hide:] +// [#protodoc-title: Wasm] +// [#extension: envoy.filters.network.wasm] +// Wasm :ref:`configuration overview `. + message Wasm { // General Plugin configuration. envoy.extensions.wasm.v3.PluginConfig config = 1; diff --git a/api/envoy/extensions/stat_sinks/wasm/v3/BUILD b/api/envoy/extensions/stat_sinks/wasm/v3/BUILD new file mode 100644 index 000000000000..c37174bdefc4 --- /dev/null +++ b/api/envoy/extensions/stat_sinks/wasm/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/extensions/wasm/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/stat_sinks/wasm/v3/wasm.proto b/api/envoy/extensions/stat_sinks/wasm/v3/wasm.proto new file mode 100644 index 000000000000..3fc5dae91795 --- /dev/null +++ b/api/envoy/extensions/stat_sinks/wasm/v3/wasm.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package envoy.extensions.stat_sinks.wasm.v3; + +import "envoy/extensions/wasm/v3/wasm.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.stat_sinks.wasm.v3"; +option java_outer_classname = "WasmProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Wasm] +// Wasm :ref:`configuration overview `. +// [#extension: envoy.stat_sinks.wasm] + +message Wasm { + // General Plugin configuration. + envoy.extensions.wasm.v3.PluginConfig config = 1; +} diff --git a/api/envoy/extensions/wasm/v3/wasm.proto b/api/envoy/extensions/wasm/v3/wasm.proto index c036603c5759..b42fb75a0bf7 100644 --- a/api/envoy/extensions/wasm/v3/wasm.proto +++ b/api/envoy/extensions/wasm/v3/wasm.proto @@ -16,8 +16,8 @@ option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Wasm] +// [#extension: envoy.bootstrap.wasm] -// [[#not-implemented-hide:] // Configuration for a Wasm VM. // [#next-free-field: 7] message VmConfig { @@ -51,7 +51,6 @@ message VmConfig { bool nack_on_code_cache_miss = 6; } -// [[#not-implemented-hide:] // Base Configuration for Wasm Plugins e.g. filters and services. // [#next-free-field: 6] message PluginConfig { @@ -66,9 +65,9 @@ message PluginConfig { string root_id = 2; // Configuration for finding or starting VM. - oneof vm_config { - VmConfig inline_vm_config = 3; - // In the future add referential VM configurations. + oneof vm { + VmConfig vm_config = 3; + // TODO: add referential VM configurations. } // Filter/service configuration used to configure or reconfigure a plugin @@ -86,7 +85,6 @@ message PluginConfig { bool fail_open = 5; } -// [[#not-implemented-hide:] // WasmService is configured as a built-in *envoy.wasm_service* :ref:`WasmService // ` This opaque configuration will be used to create a Wasm Service. message WasmService { diff --git a/api/versioning/BUILD b/api/versioning/BUILD index d5a154363088..d44a54640ca4 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -120,6 +120,7 @@ proto_library( "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/retry/host/omit_host_metadata/v3:pkg", "//envoy/extensions/retry/priority/previous_priorities/v3:pkg", + "//envoy/extensions/stat_sinks/wasm/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", "//envoy/extensions/transport_sockets/proxy_protocol/v3:pkg", "//envoy/extensions/transport_sockets/quic/v3:pkg", diff --git a/bazel/BUILD b/bazel/BUILD index 5bdd7b7520cc..67ff89205a59 100644 --- a/bazel/BUILD +++ b/bazel/BUILD @@ -287,6 +287,27 @@ config_setting( values = {"define": "quiche=enabled"}, ) +# TODO: consider converting WAVM VM support to an extension (https://github.com/envoyproxy/envoy/issues/12574) +config_setting( + name = "wasm_all", + values = {"define": "wasm=enabled"}, +) + +config_setting( + name = "wasm_wavm", + values = {"define": "wasm=wavm"}, +) + +config_setting( + name = "wasm_v8", + values = {"define": "wasm=v8"}, +) + +config_setting( + name = "wasm_none", + values = {"define": "wasm=disabled"}, +) + # Alias pointing to the selected version of BoringSSL: # - BoringSSL FIPS from @boringssl_fips//:ssl, # - non-FIPS BoringSSL from @boringssl//:ssl. diff --git a/bazel/crates.bzl b/bazel/crates.bzl new file mode 100644 index 000000000000..d4373143ddd4 --- /dev/null +++ b/bazel/crates.bzl @@ -0,0 +1,113 @@ +""" +cargo-raze crate workspace functions + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository") + +def _new_http_archive(name, **kwargs): + if not native.existing_rule(name): + http_archive(name = name, **kwargs) + +def _new_git_repository(name, **kwargs): + if not native.existing_rule(name): + new_git_repository(name = name, **kwargs) + +def raze_fetch_remote_crates(): + _new_http_archive( + name = "raze__ahash__0_3_8", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/ahash/ahash-0.3.8.crate", + type = "tar.gz", + strip_prefix = "ahash-0.3.8", + build_file = Label("//bazel/external/cargo/remote:ahash-0.3.8.BUILD"), + ) + + _new_http_archive( + name = "raze__autocfg__1_0_0", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/autocfg/autocfg-1.0.0.crate", + type = "tar.gz", + strip_prefix = "autocfg-1.0.0", + build_file = Label("//bazel/external/cargo/remote:autocfg-1.0.0.BUILD"), + ) + + _new_http_archive( + name = "raze__cfg_if__0_1_10", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/cfg-if/cfg-if-0.1.10.crate", + type = "tar.gz", + strip_prefix = "cfg-if-0.1.10", + build_file = Label("//bazel/external/cargo/remote:cfg-if-0.1.10.BUILD"), + ) + + _new_http_archive( + name = "raze__hashbrown__0_7_2", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/hashbrown/hashbrown-0.7.2.crate", + type = "tar.gz", + strip_prefix = "hashbrown-0.7.2", + build_file = Label("//bazel/external/cargo/remote:hashbrown-0.7.2.BUILD"), + ) + + _new_http_archive( + name = "raze__libc__0_2_74", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/libc/libc-0.2.74.crate", + type = "tar.gz", + strip_prefix = "libc-0.2.74", + build_file = Label("//bazel/external/cargo/remote:libc-0.2.74.BUILD"), + ) + + _new_http_archive( + name = "raze__log__0_4_11", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/log/log-0.4.11.crate", + type = "tar.gz", + strip_prefix = "log-0.4.11", + build_file = Label("//bazel/external/cargo/remote:log-0.4.11.BUILD"), + ) + + _new_http_archive( + name = "raze__memory_units__0_4_0", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/memory_units/memory_units-0.4.0.crate", + type = "tar.gz", + strip_prefix = "memory_units-0.4.0", + build_file = Label("//bazel/external/cargo/remote:memory_units-0.4.0.BUILD"), + ) + + _new_http_archive( + name = "raze__proxy_wasm__0_1_2", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/proxy-wasm/proxy-wasm-0.1.2.crate", + type = "tar.gz", + strip_prefix = "proxy-wasm-0.1.2", + build_file = Label("//bazel/external/cargo/remote:proxy-wasm-0.1.2.BUILD"), + ) + + _new_http_archive( + name = "raze__wee_alloc__0_4_5", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/wee_alloc/wee_alloc-0.4.5.crate", + type = "tar.gz", + strip_prefix = "wee_alloc-0.4.5", + build_file = Label("//bazel/external/cargo/remote:wee_alloc-0.4.5.BUILD"), + ) + + _new_http_archive( + name = "raze__winapi__0_3_9", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/winapi/winapi-0.3.9.crate", + type = "tar.gz", + strip_prefix = "winapi-0.3.9", + build_file = Label("//bazel/external/cargo/remote:winapi-0.3.9.BUILD"), + ) + + _new_http_archive( + name = "raze__winapi_i686_pc_windows_gnu__0_4_0", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/winapi-i686-pc-windows-gnu/winapi-i686-pc-windows-gnu-0.4.0.crate", + type = "tar.gz", + strip_prefix = "winapi-i686-pc-windows-gnu-0.4.0", + build_file = Label("//bazel/external/cargo/remote:winapi-i686-pc-windows-gnu-0.4.0.BUILD"), + ) + + _new_http_archive( + name = "raze__winapi_x86_64_pc_windows_gnu__0_4_0", + url = "https://crates-io.s3-us-west-1.amazonaws.com/crates/winapi-x86_64-pc-windows-gnu/winapi-x86_64-pc-windows-gnu-0.4.0.crate", + type = "tar.gz", + strip_prefix = "winapi-x86_64-pc-windows-gnu-0.4.0", + build_file = Label("//bazel/external/cargo/remote:winapi-x86_64-pc-windows-gnu-0.4.0.BUILD"), + ) diff --git a/bazel/dependency_imports.bzl b/bazel/dependency_imports.bzl index 92c837a4f06a..bb0bcf815f43 100644 --- a/bazel/dependency_imports.bzl +++ b/bazel/dependency_imports.bzl @@ -5,6 +5,8 @@ load("@bazel_toolchains//rules/exec_properties:exec_properties.bzl", "create_rbe load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies") load("@upb//bazel:repository_defs.bzl", upb_bazel_version_repository = "bazel_version_repository") +load("@io_bazel_rules_rust//rust:repositories.bzl", "rust_repositories") +load("@io_bazel_rules_rust//:workspace.bzl", "bazel_version") load("@config_validation_pip3//:requirements.bzl", config_validation_pip_install = "pip_install") load("@configs_pip3//:requirements.bzl", configs_pip_install = "pip_install") load("@headersplit_pip3//:requirements.bzl", headersplit_pip_install = "pip_install") @@ -23,8 +25,10 @@ def envoy_dependency_imports(go_version = GO_VERSION): rbe_toolchains_config() gazelle_dependencies() apple_rules_dependencies() + rust_repositories() + bazel_version(name = "bazel_version") upb_bazel_version_repository(name = "upb_bazel_version") - antlr_dependencies(471) + antlr_dependencies(472) custom_exec_properties( name = "envoy_large_machine_exec_property", diff --git a/bazel/envoy_build_system.bzl b/bazel/envoy_build_system.bzl index bdeb501e3068..e95329095dca 100644 --- a/bazel/envoy_build_system.bzl +++ b/bazel/envoy_build_system.bzl @@ -19,6 +19,10 @@ load( _envoy_select_google_grpc = "envoy_select_google_grpc", _envoy_select_hot_restart = "envoy_select_hot_restart", _envoy_select_new_codecs_in_integration_tests = "envoy_select_new_codecs_in_integration_tests", + _envoy_select_wasm = "envoy_select_wasm", + _envoy_select_wasm_all_v8_wavm_none = "envoy_select_wasm_all_v8_wavm_none", + _envoy_select_wasm_v8 = "envoy_select_wasm_v8", + _envoy_select_wasm_wavm = "envoy_select_wasm_wavm", ) load( ":envoy_test.bzl", @@ -176,6 +180,10 @@ def envoy_google_grpc_external_deps(): envoy_select_boringssl = _envoy_select_boringssl envoy_select_google_grpc = _envoy_select_google_grpc envoy_select_hot_restart = _envoy_select_hot_restart +envoy_select_wasm = _envoy_select_wasm +envoy_select_wasm_all_v8_wavm_none = _envoy_select_wasm_all_v8_wavm_none +envoy_select_wasm_wavm = _envoy_select_wasm_wavm +envoy_select_wasm_v8 = _envoy_select_wasm_v8 envoy_select_new_codecs_in_integration_tests = _envoy_select_new_codecs_in_integration_tests # Binary wrappers (from envoy_binary.bzl) diff --git a/bazel/envoy_select.bzl b/bazel/envoy_select.bzl index 107ad2a21bde..5a33e4da515d 100644 --- a/bazel/envoy_select.bzl +++ b/bazel/envoy_select.bzl @@ -32,6 +32,36 @@ def envoy_select_hot_restart(xs, repository = ""): "//conditions:default": xs, }) +# Selects the given values depending on the WASM runtimes enabled in the current build. +def envoy_select_wasm(xs): + return select({ + "@envoy//bazel:wasm_none": [], + "//conditions:default": xs, + }) + +def envoy_select_wasm_v8(xs): + return select({ + "@envoy//bazel:wasm_wavm": [], + "@envoy//bazel:wasm_none": [], + "//conditions:default": xs, + }) + +def envoy_select_wasm_wavm(xs): + return select({ + "@envoy//bazel:wasm_all": xs, + "@envoy//bazel:wasm_wavm": xs, + "//conditions:default": [], + }) + +def envoy_select_wasm_all_v8_wavm_none(xs1, xs2, xs3, xs4): + return select({ + "@envoy//bazel:wasm_all": xs1, + "@envoy//bazel:wasm_v8": xs2, + "@envoy//bazel:wasm_wavm": xs3, + "@envoy//bazel:wasm_none": xs4, + "//conditions:default": xs2, + }) + # Select the given values if use legacy codecs in test is on in the current build. def envoy_select_new_codecs_in_integration_tests(xs, repository = ""): return select({ diff --git a/bazel/external/cargo/BUILD b/bazel/external/cargo/BUILD new file mode 100644 index 000000000000..e216296d130d --- /dev/null +++ b/bazel/external/cargo/BUILD @@ -0,0 +1,23 @@ +""" +cargo-raze workspace build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +package(default_visibility = ["//visibility:public"]) + +licenses([ + "notice", # See individual crates for specific licenses +]) + +alias( + name = "log", + actual = "@raze__log__0_4_11//:log", + tags = ["cargo-raze"], +) + +alias( + name = "proxy_wasm", + actual = "@raze__proxy_wasm__0_1_2//:proxy_wasm", + tags = ["cargo-raze"], +) diff --git a/bazel/external/cargo/remote/BUILD b/bazel/external/cargo/remote/BUILD new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/bazel/external/cargo/remote/ahash-0.3.8.BUILD b/bazel/external/cargo/remote/ahash-0.3.8.BUILD new file mode 100644 index 000000000000..a34e9e1685cf --- /dev/null +++ b/bazel/external/cargo/remote/ahash-0.3.8.BUILD @@ -0,0 +1,46 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Unsupported target "ahash" with type "bench" omitted + +rust_library( + name = "ahash", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.3.8", + deps = [ + ], +) + +# Unsupported target "bench" with type "test" omitted +# Unsupported target "map" with type "bench" omitted +# Unsupported target "map_tests" with type "test" omitted +# Unsupported target "nopanic" with type "test" omitted diff --git a/bazel/external/cargo/remote/autocfg-1.0.0.BUILD b/bazel/external/cargo/remote/autocfg-1.0.0.BUILD new file mode 100644 index 000000000000..9f51a3e4cd37 --- /dev/null +++ b/bazel/external/cargo/remote/autocfg-1.0.0.BUILD @@ -0,0 +1,45 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # Apache-2.0 from expression "Apache-2.0 OR MIT" +]) + +rust_library( + name = "autocfg", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "1.0.0", + deps = [ + ], +) + +# Unsupported target "integers" with type "example" omitted +# Unsupported target "paths" with type "example" omitted +# Unsupported target "rustflags" with type "test" omitted +# Unsupported target "traits" with type "example" omitted +# Unsupported target "versions" with type "example" omitted diff --git a/bazel/external/cargo/remote/cfg-if-0.1.10.BUILD b/bazel/external/cargo/remote/cfg-if-0.1.10.BUILD new file mode 100644 index 000000000000..b36c1413e5b0 --- /dev/null +++ b/bazel/external/cargo/remote/cfg-if-0.1.10.BUILD @@ -0,0 +1,41 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +rust_library( + name = "cfg_if", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.1.10", + deps = [ + ], +) + +# Unsupported target "xcrate" with type "test" omitted diff --git a/bazel/external/cargo/remote/hashbrown-0.7.2.BUILD b/bazel/external/cargo/remote/hashbrown-0.7.2.BUILD new file mode 100644 index 000000000000..54276e05010e --- /dev/null +++ b/bazel/external/cargo/remote/hashbrown-0.7.2.BUILD @@ -0,0 +1,50 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # Apache-2.0 from expression "Apache-2.0 OR MIT" +]) + +# Unsupported target "bench" with type "bench" omitted +# Unsupported target "build-script-build" with type "custom-build" omitted + +rust_library( + name = "hashbrown", + srcs = glob(["**/*.rs"]), + crate_features = [ + "ahash", + "inline-more", + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.7.2", + deps = [ + "@raze__ahash__0_3_8//:ahash", + ], +) + +# Unsupported target "hasher" with type "test" omitted +# Unsupported target "rayon" with type "test" omitted +# Unsupported target "serde" with type "test" omitted +# Unsupported target "set" with type "test" omitted diff --git a/bazel/external/cargo/remote/libc-0.2.74.BUILD b/bazel/external/cargo/remote/libc-0.2.74.BUILD new file mode 100644 index 000000000000..76a2773d1a4c --- /dev/null +++ b/bazel/external/cargo/remote/libc-0.2.74.BUILD @@ -0,0 +1,42 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Unsupported target "build-script-build" with type "custom-build" omitted +# Unsupported target "const_fn" with type "test" omitted + +rust_library( + name = "libc", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.2.74", + deps = [ + ], +) diff --git a/bazel/external/cargo/remote/log-0.4.11.BUILD b/bazel/external/cargo/remote/log-0.4.11.BUILD new file mode 100644 index 000000000000..9596e2448ecc --- /dev/null +++ b/bazel/external/cargo/remote/log-0.4.11.BUILD @@ -0,0 +1,46 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Unsupported target "build-script-build" with type "custom-build" omitted +# Unsupported target "filters" with type "test" omitted + +rust_library( + name = "log", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + "--cfg=atomic_cas", + ], + tags = ["cargo-raze"], + version = "0.4.11", + deps = [ + "@raze__cfg_if__0_1_10//:cfg_if", + ], +) + +# Unsupported target "macros" with type "test" omitted diff --git a/bazel/external/cargo/remote/memory_units-0.4.0.BUILD b/bazel/external/cargo/remote/memory_units-0.4.0.BUILD new file mode 100644 index 000000000000..c5c3c3987128 --- /dev/null +++ b/bazel/external/cargo/remote/memory_units-0.4.0.BUILD @@ -0,0 +1,39 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "reciprocal", # MPL-2.0 from expression "MPL-2.0" +]) + +rust_library( + name = "memory_units", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.4.0", + deps = [ + ], +) diff --git a/bazel/external/cargo/remote/proxy-wasm-0.1.2.BUILD b/bazel/external/cargo/remote/proxy-wasm-0.1.2.BUILD new file mode 100644 index 000000000000..2f9895fea7fa --- /dev/null +++ b/bazel/external/cargo/remote/proxy-wasm-0.1.2.BUILD @@ -0,0 +1,47 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # Apache-2.0 from expression "Apache-2.0" +]) + +# Unsupported target "hello_world" with type "example" omitted +# Unsupported target "http_auth_random" with type "example" omitted +# Unsupported target "http_body" with type "example" omitted +# Unsupported target "http_headers" with type "example" omitted + +rust_library( + name = "proxy_wasm", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.1.2", + deps = [ + "@raze__hashbrown__0_7_2//:hashbrown", + "@raze__log__0_4_11//:log", + "@raze__wee_alloc__0_4_5//:wee_alloc", + ], +) diff --git a/bazel/external/cargo/remote/wee_alloc-0.4.5.BUILD b/bazel/external/cargo/remote/wee_alloc-0.4.5.BUILD new file mode 100644 index 000000000000..ab49873603cd --- /dev/null +++ b/bazel/external/cargo/remote/wee_alloc-0.4.5.BUILD @@ -0,0 +1,46 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "reciprocal", # MPL-2.0 from expression "MPL-2.0" +]) + +# Unsupported target "build-script-build" with type "custom-build" omitted + +rust_library( + name = "wee_alloc", + srcs = glob(["**/*.rs"]), + crate_features = [ + "default", + "size_classes", + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.4.5", + deps = [ + "@raze__cfg_if__0_1_10//:cfg_if", + "@raze__libc__0_2_74//:libc", + "@raze__memory_units__0_4_0//:memory_units", + ], +) diff --git a/bazel/external/cargo/remote/winapi-0.3.9.BUILD b/bazel/external/cargo/remote/winapi-0.3.9.BUILD new file mode 100644 index 000000000000..2495dd1d900e --- /dev/null +++ b/bazel/external/cargo/remote/winapi-0.3.9.BUILD @@ -0,0 +1,44 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Unsupported target "build-script-build" with type "custom-build" omitted + +rust_library( + name = "winapi", + srcs = glob(["**/*.rs"]), + crate_features = [ + "memoryapi", + "synchapi", + "winbase", + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.3.9", + deps = [ + ], +) diff --git a/bazel/external/cargo/remote/winapi-i686-pc-windows-gnu-0.4.0.BUILD b/bazel/external/cargo/remote/winapi-i686-pc-windows-gnu-0.4.0.BUILD new file mode 100644 index 000000000000..d6c1545143fe --- /dev/null +++ b/bazel/external/cargo/remote/winapi-i686-pc-windows-gnu-0.4.0.BUILD @@ -0,0 +1,41 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Unsupported target "build-script-build" with type "custom-build" omitted + +rust_library( + name = "winapi_i686_pc_windows_gnu", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.4.0", + deps = [ + ], +) diff --git a/bazel/external/cargo/remote/winapi-x86_64-pc-windows-gnu-0.4.0.BUILD b/bazel/external/cargo/remote/winapi-x86_64-pc-windows-gnu-0.4.0.BUILD new file mode 100644 index 000000000000..e666870dbd05 --- /dev/null +++ b/bazel/external/cargo/remote/winapi-x86_64-pc-windows-gnu-0.4.0.BUILD @@ -0,0 +1,41 @@ +""" +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_library", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//bazel/external/cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Unsupported target "build-script-build" with type "custom-build" omitted + +rust_library( + name = "winapi_x86_64_pc_windows_gnu", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = ["cargo-raze"], + version = "0.4.0", + deps = [ + ], +) diff --git a/bazel/external/proxy_wasm_cpp_host.BUILD b/bazel/external/proxy_wasm_cpp_host.BUILD index 4cb87cf98ec1..1b3f0829d7b2 100644 --- a/bazel/external/proxy_wasm_cpp_host.BUILD +++ b/bazel/external/proxy_wasm_cpp_host.BUILD @@ -1,4 +1,10 @@ load("@rules_cc//cc:defs.bzl", "cc_library") +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_select_wasm_all_v8_wavm_none", + "envoy_select_wasm_v8", + "envoy_select_wasm_wavm", +) licenses(["notice"]) # Apache 2 @@ -14,14 +20,44 @@ cc_library( cc_library( name = "lib", - srcs = glob( - [ - "src/**/*.h", - "src/**/*.cc", - ], - exclude = ["src/**/wavm*"], + # Note that the select cannot appear in the glob. + srcs = envoy_select_wasm_all_v8_wavm_none( + glob( + [ + "src/**/*.h", + "src/**/*.cc", + ], + ), + glob( + [ + "src/**/*.h", + "src/**/*.cc", + ], + exclude = ["src/wavm/*"], + ), + glob( + [ + "src/**/*.h", + "src/**/*.cc", + ], + exclude = ["src/v8/*"], + ), + glob( + [ + "src/**/*.h", + "src/**/*.cc", + ], + exclude = [ + "src/wavm/*", + "src/v8/*", + ], + ), ), - copts = ["-std=c++14"], + copts = envoy_select_wasm_wavm([ + '-DWAVM_API=""', + "-Wno-non-virtual-dtor", + "-Wno-old-style-cast", + ]), deps = [ ":include", "//external:abseil_flat_hash_map", @@ -29,9 +65,12 @@ cc_library( "//external:abseil_strings", "//external:protobuf", "//external:ssl", - "//external:wee8", "//external:zlib", "@proxy_wasm_cpp_sdk//:api_lib", "@proxy_wasm_cpp_sdk//:common_lib", - ], + ] + envoy_select_wasm_wavm([ + "@envoy//bazel/foreign_cc:wavm", + ]) + envoy_select_wasm_v8([ + "//external:wee8", + ]), ) diff --git a/bazel/external/wee8.BUILD b/bazel/external/wee8.BUILD index b61f95748672..2cc17dcb2374 100644 --- a/bazel/external/wee8.BUILD +++ b/bazel/external/wee8.BUILD @@ -13,6 +13,7 @@ cc_library( "wee8/include/v8-version.h", "wee8/third_party/wasm-api/wasm.hh", ], + defines = ["ENVOY_WASM_V8"], includes = [ "wee8/include", "wee8/third_party", diff --git a/bazel/external/wee8.genrule_cmd b/bazel/external/wee8.genrule_cmd index 8cb0e24c5f49..cb688bcf45f9 100644 --- a/bazel/external/wee8.genrule_cmd +++ b/bazel/external/wee8.genrule_cmd @@ -19,7 +19,7 @@ pushd $$ROOT/wee8 rm -rf out/wee8 # Export compiler configuration. -export CXXFLAGS="$${CXXFLAGS-} -Wno-deprecated-copy -Wno-unknown-warning-option" +export CXXFLAGS="$${CXXFLAGS-} -Wno-sign-compare -Wno-deprecated-copy -Wno-unknown-warning-option" if [[ ( `uname` == "Darwin" && $${CXX-} == "" ) || $${CXX-} == *"clang"* ]]; then export IS_CLANG=true export CC=$${CC:-clang} diff --git a/bazel/external/wee8.patch b/bazel/external/wee8.patch index ad1c20b6c00b..cce3eecde614 100644 --- a/bazel/external/wee8.patch +++ b/bazel/external/wee8.patch @@ -34,7 +34,7 @@ #endif --- wee8/build/config/sanitizers/sanitizers.gni +++ wee8/build/config/sanitizers/sanitizers.gni -@@ -147,7 +147,7 @@ if (!is_a_target_toolchain) { +@@ -150,7 +150,7 @@ if (!is_a_target_toolchain) { # standard system libraries. We have instrumented system libraries for msan, # which requires them to prevent false positives. # TODO(thakis): Maybe remove this variable. @@ -43,7 +43,7 @@ # Whether we are doing a fuzzer build. Normally this should be checked instead # of checking "use_libfuzzer || use_afl" because often developers forget to -@@ -195,8 +195,7 @@ assert(!using_sanitizer || is_clang, +@@ -198,8 +198,7 @@ assert(!using_sanitizer || is_clang, assert(!is_cfi || is_clang, "is_cfi requires setting is_clang = true in 'gn args'") diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index 096619703604..907098af180c 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -154,8 +154,8 @@ envoy_cmake_external( deps = [ ":ares", ":nghttp2", - ":zlib", "//external:ssl", + "//external:zlib", ], ) @@ -197,6 +197,109 @@ envoy_cmake_external( }), ) +envoy_cmake_external( + name = "llvm", + cache_entries = { + # Disable both: BUILD and INCLUDE, since some of the INCLUDE + # targets build code instead of only generating build files. + "LLVM_BUILD_DOCS": "off", + "LLVM_INCLUDE_DOCS": "off", + "LLVM_BUILD_EXAMPLES": "off", + "LLVM_INCLUDE_EXAMPLES": "off", + "LLVM_BUILD_RUNTIME": "off", + "LLVM_BUILD_RUNTIMES": "off", + "LLVM_INCLUDE_RUNTIMES": "off", + "LLVM_BUILD_TESTS": "off", + "LLVM_INCLUDE_TESTS": "off", + "LLVM_BUILD_TOOLS": "off", + "LLVM_INCLUDE_TOOLS": "off", + "LLVM_BUILD_UTILS": "off", + "LLVM_INCLUDE_UTILS": "off", + "LLVM_ENABLE_LIBEDIT": "off", + "LLVM_ENABLE_LIBXML2": "off", + "LLVM_ENABLE_TERMINFO": "off", + "LLVM_ENABLE_ZLIB": "off", + "LLVM_TARGETS_TO_BUILD": "X86", + "CMAKE_CXX_COMPILER_FORCED": "on", + # Workaround for the issue with statically linked libstdc++ + # using -l:libstdc++.a. + "CMAKE_CXX_FLAGS": "-lstdc++", + }, + env_vars = { + # Workaround for the -DDEBUG flag added in fastbuild on macOS, + # which conflicts with DEBUG macro used in LLVM. + "CFLAGS": "-UDEBUG", + "CXXFLAGS": "-UDEBUG", + "ASMFLAGS": "-UDEBUG", + }, + lib_source = "@org_llvm_llvm//:all", + static_libraries = select({ + "//conditions:default": [ + # Order from llvm-config --libnames. + "libLLVMLTO.a", + "libLLVMPasses.a", + "libLLVMObjCARCOpts.a", + "libLLVMSymbolize.a", + "libLLVMDebugInfoPDB.a", + "libLLVMDebugInfoDWARF.a", + "libLLVMFuzzMutate.a", + "libLLVMTableGen.a", + "libLLVMDlltoolDriver.a", + "libLLVMLineEditor.a", + "libLLVMOrcJIT.a", + "libLLVMCoverage.a", + "libLLVMMIRParser.a", + "libLLVMObjectYAML.a", + "libLLVMLibDriver.a", + "libLLVMOption.a", + "libLLVMWindowsManifest.a", + "libLLVMX86Disassembler.a", + "libLLVMX86AsmParser.a", + "libLLVMX86CodeGen.a", + "libLLVMGlobalISel.a", + "libLLVMSelectionDAG.a", + "libLLVMAsmPrinter.a", + "libLLVMDebugInfoCodeView.a", + "libLLVMDebugInfoMSF.a", + "libLLVMX86Desc.a", + "libLLVMMCDisassembler.a", + "libLLVMX86Info.a", + "libLLVMX86Utils.a", + "libLLVMMCJIT.a", + "libLLVMInterpreter.a", + "libLLVMExecutionEngine.a", + "libLLVMRuntimeDyld.a", + "libLLVMCodeGen.a", + "libLLVMTarget.a", + "libLLVMCoroutines.a", + "libLLVMipo.a", + "libLLVMInstrumentation.a", + "libLLVMVectorize.a", + "libLLVMScalarOpts.a", + "libLLVMLinker.a", + "libLLVMIRReader.a", + "libLLVMAsmParser.a", + "libLLVMInstCombine.a", + "libLLVMTransformUtils.a", + "libLLVMBitWriter.a", + "libLLVMAnalysis.a", + "libLLVMProfileData.a", + "libLLVMObject.a", + "libLLVMMCParser.a", + "libLLVMMC.a", + "libLLVMBitReader.a", + "libLLVMBitstreamReader.a", + "libLLVMCore.a", + "libLLVMBinaryFormat.a", + "libLLVMSupport.a", + "libLLVMDemangle.a", + "libLLVMRemarks.a", + "libLLVMCFGuard.a", + "libLLVMTextAPI.a", + ], + }), +) + envoy_cmake_external( name = "nghttp2", cache_entries = { @@ -216,6 +319,35 @@ envoy_cmake_external( }), ) +envoy_cmake_external( + name = "wavm", + binaries = ["wavm"], + cache_entries = { + "LLVM_DIR": "$EXT_BUILD_DEPS/copy_llvm/llvm/lib/cmake/llvm", + "WAVM_ENABLE_STATIC_LINKING": "on", + "WAVM_ENABLE_RELEASE_ASSERTS": "on", + "WAVM_ENABLE_UNWIND": "no", + # Workaround for the issue with statically linked libstdc++ + # using -l:libstdc++.a. + "CMAKE_CXX_FLAGS": "-lstdc++ -Wno-unused-command-line-argument", + }, + defines = ["ENVOY_WASM_WAVM"], + env_vars = { + # Workaround for the -DDEBUG flag added in fastbuild on macOS, + # which conflicts with DEBUG macro used in LLVM. + "CFLAGS": "-UDEBUG", + "CXXFLAGS": "-UDEBUG", + "ASMFLAGS": "-UDEBUG", + }, + lib_source = "@com_github_wavm_wavm//:all", + static_libraries = select({ + "//conditions:default": [ + "libWAVM.a", + ], + }), + deps = [":llvm"], +) + envoy_cmake_external( name = "zlib", cache_entries = { diff --git a/bazel/foreign_cc/llvm.patch b/bazel/foreign_cc/llvm.patch new file mode 100644 index 000000000000..cd02f2842401 --- /dev/null +++ b/bazel/foreign_cc/llvm.patch @@ -0,0 +1,25 @@ +# Workaround for Envoy's CMAKE_BUILD_TYPE=Bazel. +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -247,7 +247,7 @@ + string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE) + + if (CMAKE_BUILD_TYPE AND +- NOT uppercase_CMAKE_BUILD_TYPE MATCHES "^(DEBUG|RELEASE|RELWITHDEBINFO|MINSIZEREL)$") ++ NOT uppercase_CMAKE_BUILD_TYPE MATCHES "^(DEBUG|RELEASE|RELWITHDEBINFO|MINSIZEREL|BAZEL)$") + message(FATAL_ERROR "Invalid value for CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") + endif() + +# Workaround for a missing -fuse-ld flag in CXXFLAGS, which results in +# different linkers being used during configure and compilation phases. +--- a/cmake/modules/HandleLLVMOptions.cmake ++++ b/cmake/modules/HandleLLVMOptions.cmake +@@ -718,8 +718,6 @@ endif() + if (UNIX AND CMAKE_GENERATOR STREQUAL "Ninja") + include(CheckLinkerFlag) + check_linker_flag("-Wl,--color-diagnostics" LINKER_SUPPORTS_COLOR_DIAGNOSTICS) +- append_if(LINKER_SUPPORTS_COLOR_DIAGNOSTICS "-Wl,--color-diagnostics" +- CMAKE_EXE_LINKER_FLAGS CMAKE_MODULE_LINKER_FLAGS CMAKE_SHARED_LINKER_FLAGS) + endif() + + # Add flags for add_dead_strip(). diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index 64d61ea49940..2e1b6280b5b7 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -4,6 +4,7 @@ load(":genrule_repository.bzl", "genrule_repository") load("@envoy_api//bazel:envoy_http_archive.bzl", "envoy_http_archive") load(":repository_locations.bzl", "DEPENDENCY_ANNOTATIONS", "DEPENDENCY_REPOSITORIES", "USE_CATEGORIES", "USE_CATEGORIES_WITH_CPE_OPTIONAL") load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") +load(":crates.bzl", "raze_fetch_remote_crates") PPC_SKIP_TARGETS = ["envoy.filters.http.lua"] @@ -19,6 +20,9 @@ WINDOWS_SKIP_TARGETS = [ # archives, e.g. cares. BUILD_ALL_CONTENT = """filegroup(name = "all", srcs = glob(["**"]), visibility = ["//visibility:public"])""" +def _build_all_content(exclude = []): + return """filegroup(name = "all", srcs = glob(["**"], exclude={}), visibility = ["//visibility:public"])""".format(repr(exclude)) + def _fail_missing_attribute(attr, key): fail("The '%s' attribute must be defined for external dependecy " % attr + key) @@ -161,6 +165,10 @@ def _go_deps(skip_targets): ) _repository_impl("bazel_gazelle") +def _rust_deps(): + _repository_impl("io_bazel_rules_rust") + raze_fetch_remote_crates() + def envoy_dependencies(skip_targets = []): # Setup Envoy developer tools. envoy_dev_binding() @@ -238,8 +246,12 @@ def envoy_dependencies(skip_targets = []): _python_deps() _cc_deps() _go_deps(skip_targets) + _rust_deps() _kafka_deps() + _org_llvm_llvm() + _com_github_wavm_wavm() + switched_rules_by_language( name = "com_google_googleapis_imports", cc = True, @@ -434,6 +446,34 @@ cc_library( **location ) + # Parser dependencies + # TODO: upgrade this when cel is upgraded to use the latest version + http_archive( + name = "rules_antlr", + sha256 = "7249d1569293d9b239e23c65f6b4c81a07da921738bde0dfeb231ed98be40429", + strip_prefix = "rules_antlr-3cc2f9502a54ceb7b79b37383316b23c4da66f9a", + urls = ["https://github.com/marcohu/rules_antlr/archive/3cc2f9502a54ceb7b79b37383316b23c4da66f9a.tar.gz"], + ) + + http_archive( + name = "antlr4_runtimes", + build_file_content = """ +package(default_visibility = ["//visibility:public"]) +cc_library( + name = "cpp", + srcs = glob(["runtime/Cpp/runtime/src/**/*.cpp"]), + hdrs = glob(["runtime/Cpp/runtime/src/**/*.h"]), + includes = ["runtime/Cpp/runtime/src"], +) +""", + sha256 = "46f5e1af5f4bd28ade55cb632f9a069656b31fc8c2408f9aa045f9b5f5caad64", + patch_args = ["-p1"], + # Patches ASAN violation of initialization fiasco + patches = ["@envoy//bazel:antlr.patch"], + strip_prefix = "antlr4-4.7.2", + urls = ["https://github.com/antlr/antlr4/archive/4.7.2.tar.gz"], + ) + def _com_github_nghttp2_nghttp2(): location = _get_location("com_github_nghttp2_nghttp2") http_archive( @@ -832,7 +872,10 @@ def _proxy_wasm_cpp_host(): def _emscripten_toolchain(): _repository_impl( name = "emscripten_toolchain", - build_file_content = BUILD_ALL_CONTENT, + build_file_content = _build_all_content(exclude = [ + "upstream/emscripten/cache/is_vanilla.txt", + ".emscripten_sanity", + ]), patch_cmds = REPOSITORY_LOCATIONS["emscripten_toolchain"]["patch_cmds"], ) @@ -899,6 +942,32 @@ def _com_github_gperftools_gperftools(): actual = "@envoy//bazel/foreign_cc:gperftools", ) +def _org_llvm_llvm(): + location = _get_location("org_llvm_llvm") + http_archive( + name = "org_llvm_llvm", + build_file_content = BUILD_ALL_CONTENT, + patch_args = ["-p1"], + patches = ["@envoy//bazel/foreign_cc:llvm.patch"], + **location + ) + native.bind( + name = "llvm", + actual = "@envoy//bazel/foreign_cc:llvm", + ) + +def _com_github_wavm_wavm(): + location = _get_location("com_github_wavm_wavm") + http_archive( + name = "com_github_wavm_wavm", + build_file_content = BUILD_ALL_CONTENT, + **location + ) + native.bind( + name = "wavm", + actual = "@envoy//bazel/foreign_cc:wavm", + ) + def _kafka_deps(): # This archive contains Kafka client source code. # We are using request/response message format files to generate parser code. diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 6eba5a821d1d..07038daa5a72 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -567,8 +567,8 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( version = "0.23.7", sha256 = "0310e837aed522875791750de44408ec91046c630374990edd51827cb169f616", urls = ["https://github.com/bazelbuild/rules_go/releases/download/v{version}/rules_go-v{version}.tar.gz"], - last_updated = "2020-08-06", use_category = ["build"], + last_updated = "2020-08-06", ), rules_cc = dict( project_name = "C++ rules for Bazel", @@ -615,6 +615,44 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( last_updated = "2019-11-17", use_category = ["other"], ), + org_llvm_llvm = dict( + project_name = "LLVM", + project_desc = "LLVM Compiler Infrastructure", + project_url = "https://llvm.org", + version = "10.0", + sha256 = "df83a44b3a9a71029049ec101fb0077ecbbdf5fe41e395215025779099a98fdf", + strip_prefix = "llvm-{version}.0.src", + urls = ["https://github.com/llvm/llvm-project/releases/download/llvmorg-{version}.0/llvm-{version}.0.src.tar.xz"], + last_updated = "2020-03-24", + use_category = ["dataplane_ext"], + extensions = [ + "envoy.access_loggers.wasm", + "envoy.bootstrap.wasm", + "envoy.filters.http.wasm", + "envoy.filters.network.wasm", + "envoy.stat_sinks.wasm", + ], + cpe = "N/A", + ), + com_github_wavm_wavm = dict( + project_name = "WAVM", + project_desc = "WebAssembly Virtual Machine", + project_url = "https://wavm.github.io", + version = "e8155f1f3af88b4d08802716a7054950ef18d827", + sha256 = "cc3fcaf05d57010c9cf8eb920234679dede6c780137b55001fd34e4d14806f7c", + strip_prefix = "WAVM-{version}", + urls = ["https://github.com/WAVM/WAVM/archive/{version}.tar.gz"], + last_updated = "2020-07-06", + use_category = ["dataplane_ext"], + extensions = [ + "envoy.access_loggers.wasm", + "envoy.bootstrap.wasm", + "envoy.filters.http.wasm", + "envoy.filters.network.wasm", + "envoy.stat_sinks.wasm", + ], + cpe = "N/A", + ), io_opencensus_cpp = dict( project_name = "OpenCensus C++", project_desc = "OpenCensus tracing library", @@ -703,8 +741,13 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( urls = ["https://github.com/google/cel-cpp/archive/{version}.tar.gz"], use_category = ["dataplane_ext"], extensions = [ + "envoy.access_loggers.wasm", + "envoy.bootstrap.wasm", "envoy.filters.http.rbac", + "envoy.filters.http.wasm", "envoy.filters.network.rbac", + "envoy.filters.network.wasm", + "envoy.stat_sinks.wasm", ], last_updated = "2020-07-14", cpe = "N/A", @@ -806,9 +849,10 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( project_name = "WebAssembly for Proxies (C++ SDK)", project_desc = "WebAssembly for Proxies (C++ SDK)", project_url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk", - version = "5cec30b448975e1fd3f4117311f0957309df5cb0", - sha256 = "7d9e1f2e299215ed3e5fa8c8149740872b1100cfe3230fc639f967d9dcfd812e", + version = "7afb39d868a973caa6216a535c24e37fb666b6f3", + sha256 = "213d0b441bcc3df2c87933b24a593b5fd482fa8f4db158b707c60005b9e70040", strip_prefix = "proxy-wasm-cpp-sdk-{version}", + # 2020-09-10 urls = ["https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/{version}.tar.gz"], use_category = ["dataplane_ext"], extensions = [ @@ -825,8 +869,9 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( project_name = "WebAssembly for Proxies (C++ host implementation)", project_desc = "WebAssembly for Proxies (C++ host implementation)", project_url = "https://github.com/proxy-wasm/proxy-wasm-cpp-host", - version = "928db4d79ec7b90aea3ad13ea5df36dc60c9c31d", - sha256 = "494d3f81156b92bac640c26000497fbf3a7b1bc35f9789594280450c6e5d8129", + # 2020-09-10 + version = "49ed20e895b728aae6b811950a2939ecbaf76f7c", + sha256 = "fa03293d01450b9164f8f56ef9227301f7d1af4f373f996400f75c93f6ebc822", strip_prefix = "proxy-wasm-cpp-host-{version}", urls = ["https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/{version}.tar.gz"], use_category = ["dataplane_ext"], @@ -840,18 +885,31 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( last_updated = "2020-07-29", cpe = "N/A", ), + # TODO: upgrade to the latest version (1.41 currently fails tests) emscripten_toolchain = dict( project_name = "Emscripten SDK", project_desc = "Emscripten SDK (use by Wasm)", project_url = "https://github.com/emscripten-core/emsdk", - version = "dec8a63594753fe5f4ad3b47850bf64d66c14a4e", - sha256 = "2bdbee6947e32ad1e03cd075b48fda493ab16157b2b0225b445222cd528e1843", + version = "1.39", + sha256 = "4ac0f1f3de8b3f1373d435cd7e58bd94de4146e751f099732167749a229b443b", patch_cmds = [ - "./emsdk install 1.39.19-upstream", - "./emsdk activate --embedded 1.39.19-upstream", + "[[ \"$(uname -m)\" == \"x86_64\" ]] && ./emsdk install 1.39.6-upstream && ./emsdk activate --embedded 1.39.6-upstream || true", ], - strip_prefix = "emsdk-{version}", - urls = ["https://github.com/emscripten-core/emsdk/archive/{version}.tar.gz"], + strip_prefix = "emsdk-{version}.6", + urls = ["https://github.com/emscripten-core/emsdk/archive/{version}.6.tar.gz"], + use_category = ["build"], + last_updated = "2020-07-29", + ), + io_bazel_rules_rust = dict( + project_name = "Bazel rust rules", + project_desc = "Bazel rust rules (used by Wasm)", + project_url = "https://github.com/bazelbuild/rules_rust", + version = "fda9a1ce6482973adfda022cadbfa6b300e269c3", + sha256 = "484a2b2b67cd2d1fa1054876de7f8d291c4b203fd256bc8cbea14d749bb864ce", + # Last commit where "out_binary = True" works. + # See: https://github.com/bazelbuild/rules_rust/issues/386 + strip_prefix = "rules_rust-{version}", + urls = ["https://github.com/bazelbuild/rules_rust/archive/{version}.tar.gz"], use_category = ["build"], last_updated = "2020-07-29", ), @@ -863,8 +921,17 @@ DEPENDENCY_REPOSITORIES_SPEC = dict( sha256 = "7249d1569293d9b239e23c65f6b4c81a07da921738bde0dfeb231ed98be40429", strip_prefix = "rules_antlr-{version}", urls = ["https://github.com/marcohu/rules_antlr/archive/{version}.tar.gz"], - use_category = ["build"], + # This should be "build", but that trips the verification in the docs. + use_category = ["dataplane_ext"], + extensions = [ + "envoy.access_loggers.wasm", + "envoy.bootstrap.wasm", + "envoy.filters.http.wasm", + "envoy.filters.network.wasm", + "envoy.stat_sinks.wasm", + ], last_updated = "2020-07-29", + cpe = "N/A", ), antlr4_runtimes = dict( project_name = "ANTLR v4", diff --git a/bazel/wasm/wasm.bzl b/bazel/wasm/wasm.bzl index 65fefcb49e90..0e7a84da2e75 100644 --- a/bazel/wasm/wasm.bzl +++ b/bazel/wasm/wasm.bzl @@ -1,6 +1,7 @@ +load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary") load("@rules_cc//cc:defs.bzl", "cc_binary") -def _wasm_transition_impl(settings, attr): +def _wasm_cc_transition_impl(settings, attr): return { "//command_line_option:cpu": "wasm32", "//command_line_option:crosstool_top": "@proxy_wasm_cpp_sdk//toolchain:emscripten", @@ -11,46 +12,89 @@ def _wasm_transition_impl(settings, attr): "//command_line_option:cxxopt": [], "//command_line_option:linkopt": [], "//command_line_option:collect_code_coverage": "false", + "//command_line_option:fission": "no", } -wasm_transition = transition( - implementation = _wasm_transition_impl, +def _wasm_rust_transition_impl(settings, attr): + return { + "//command_line_option:platforms": "@io_bazel_rules_rust//rust/platform:wasm", + } + +wasm_cc_transition = transition( + implementation = _wasm_cc_transition_impl, inputs = [], outputs = [ "//command_line_option:cpu", "//command_line_option:crosstool_top", "//command_line_option:copt", "//command_line_option:cxxopt", + "//command_line_option:fission", "//command_line_option:linkopt", "//command_line_option:collect_code_coverage", ], ) +wasm_rust_transition = transition( + implementation = _wasm_rust_transition_impl, + inputs = [], + outputs = [ + "//command_line_option:platforms", + ], +) + def _wasm_binary_impl(ctx): out = ctx.actions.declare_file(ctx.label.name) - ctx.actions.run_shell( - command = 'cp "{}" "{}"'.format(ctx.files.binary[0].path, out.path), - outputs = [out], - inputs = ctx.files.binary, - ) + if ctx.attr.precompile: + ctx.actions.run( + executable = ctx.executable._compile_tool, + arguments = [ctx.files.binary[0].path, out.path], + outputs = [out], + inputs = ctx.files.binary, + ) + else: + ctx.actions.run( + executable = "cp", + arguments = [ctx.files.binary[0].path, out.path], + outputs = [out], + inputs = ctx.files.binary, + ) - return [DefaultInfo(runfiles = ctx.runfiles([out]))] + return [DefaultInfo(files = depset([out]), runfiles = ctx.runfiles([out]))] + +def _wasm_attrs(transition): + return { + "binary": attr.label(mandatory = True, cfg = transition), + "precompile": attr.bool(default = False), + # This is deliberately in target configuration to avoid compiling v8 twice. + "_compile_tool": attr.label(default = "@envoy//test/tools/wee8_compile:wee8_compile_tool", executable = True, cfg = "target"), + "_whitelist_function_transition": attr.label(default = "@bazel_tools//tools/whitelists/function_transition_whitelist"), + } # WASM binary rule implementation. # This copies the binary specified in binary attribute in WASM configuration to # target configuration, so a binary in non-WASM configuration can depend on them. -wasm_binary = rule( +wasm_cc_binary_rule = rule( implementation = _wasm_binary_impl, - attrs = { - "binary": attr.label(mandatory = True, cfg = wasm_transition), - "_whitelist_function_transition": attr.label(default = "@bazel_tools//tools/whitelists/function_transition_whitelist"), - }, + attrs = _wasm_attrs(wasm_cc_transition), +) + +wasm_rust_binary_rule = rule( + implementation = _wasm_binary_impl, + attrs = _wasm_attrs(wasm_rust_transition), ) -def wasm_cc_binary(name, **kwargs): +def wasm_cc_binary(name, tags = [], repository = "", **kwargs): wasm_name = "_wasm_" + name - kwargs.setdefault("additional_linker_inputs", ["@proxy_wasm_cpp_sdk//:jslib"]) - kwargs.setdefault("linkopts", ["--js-library external/proxy_wasm_cpp_sdk/proxy_wasm_intrinsics.js"]) + kwargs.setdefault("additional_linker_inputs", ["@proxy_wasm_cpp_sdk//:jslib", "@envoy//source/extensions/common/wasm/ext:jslib"]) + + if repository == "@envoy": + envoy_js = "--js-library source/extensions/common/wasm/ext/envoy_wasm_intrinsics.js" + else: + envoy_js = "--js-library external/envoy/source/extensions/common/wasm/ext/envoy_wasm_intrinsics.js" + kwargs.setdefault("linkopts", [ + envoy_js, + "--js-library external/proxy_wasm_cpp_sdk/proxy_wasm_intrinsics.js", + ]) kwargs.setdefault("visibility", ["//visibility:public"]) cc_binary( name = wasm_name, @@ -61,7 +105,34 @@ def wasm_cc_binary(name, **kwargs): **kwargs ) - wasm_binary( + wasm_cc_binary_rule( + name = name, + binary = ":" + wasm_name, + tags = tags + ["manual"], + ) + +def envoy_wasm_cc_binary(name, tags = [], **kwargs): + wasm_cc_binary(name, tags, repository = "@envoy", **kwargs) + +def wasm_rust_binary(name, tags = [], **kwargs): + wasm_name = "_wasm_" + (name if not ".wasm" in name else name.strip(".wasm")) + kwargs.setdefault("visibility", ["//visibility:public"]) + + rust_binary( + name = wasm_name, + edition = "2018", + crate_type = "cdylib", + out_binary = True, + tags = ["manual"], + **kwargs + ) + + wasm_rust_binary_rule( name = name, + precompile = select({ + "@envoy//bazel:linux_x86_64": True, + "//conditions:default": False, + }), binary = ":" + wasm_name, + tags = tags + ["manual"], ) diff --git a/ci/build_setup.sh b/ci/build_setup.sh index b64e85a6b50d..f9275c2543c8 100755 --- a/ci/build_setup.sh +++ b/ci/build_setup.sh @@ -103,6 +103,7 @@ BAZEL_BUILD_OPTIONS=( "${BAZEL_EXTRA_TEST_OPTIONS[@]}") [[ "${ENVOY_BUILD_ARCH}" == "aarch64" ]] && BAZEL_BUILD_OPTIONS+=( + "--define" "wasm=disabled" "--flaky_test_attempts=2" "--test_env=HEAPCHECK=") diff --git a/ci/do_ci.sh b/ci/do_ci.sh index f470f60c19e9..d9380c23cafd 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -211,7 +211,7 @@ elif [[ "$CI_TARGET" == "bazel.debug.server_only" ]]; then exit 0 elif [[ "$CI_TARGET" == "bazel.asan" ]]; then setup_clang_toolchain - BAZEL_BUILD_OPTIONS+=(-c dbg "--config=clang-asan" "--build_tests_only") + BAZEL_BUILD_OPTIONS+=(-c opt --copt -g "--config=clang-asan" "--build_tests_only") echo "bazel ASAN/UBSAN debug build with tests" echo "Building and testing envoy tests ${TEST_TARGETS[*]}" bazel_with_collection test "${BAZEL_BUILD_OPTIONS[@]}" "${TEST_TARGETS[@]}" @@ -279,6 +279,7 @@ elif [[ "$CI_TARGET" == "bazel.compile_time_options" ]]; then "--define" "boringssl=fips" "--define" "log_debug_assert_in_release=enabled" "--define" "quiche=enabled" + "--define" "wasm=disabled" "--define" "path_normalization_by_default=true" "--define" "deprecated_features=disabled" "--define" "use_new_codecs_in_integration_tests=true" diff --git a/ci/run_clang_tidy.sh b/ci/run_clang_tidy.sh index 3e3c97961f8b..040b5a46b895 100755 --- a/ci/run_clang_tidy.sh +++ b/ci/run_clang_tidy.sh @@ -61,8 +61,28 @@ function exclude_third_party() { grep -v third_party/ } +# Exclude files which are part of the Wasm emscripten environment +function exclude_wasm_emscripten() { + grep -v source/extensions/common/wasm/ext +} + +# Exclude files which are part of the Wasm SDK +function exclude_wasm_sdk() { + grep -v proxy_wasm_cpp_sdk +} + +# Exclude files which are part of the Wasm Host environment +function exclude_wasm_host() { + grep -v proxy_wasm_cpp_host +} + +# Exclude proxy-wasm test_data. +function exclude_wasm_test_data() { + grep -v wasm/test_data +} + function filter_excludes() { - exclude_check_format_testdata | exclude_headersplit_testdata | exclude_chromium_url | exclude_win32_impl | exclude_macos_impl | exclude_third_party + exclude_check_format_testdata | exclude_headersplit_testdata | exclude_chromium_url | exclude_win32_impl | exclude_macos_impl | exclude_third_party | exclude_wasm_emscripten | exclude_wasm_sdk | exclude_wasm_host | exclude_wasm_test_data } function run_clang_tidy() { diff --git a/ci/verify_examples.sh b/ci/verify_examples.sh index 03a26be026a3..d034a4a30cec 100755 --- a/ci/verify_examples.sh +++ b/ci/verify_examples.sh @@ -3,6 +3,7 @@ TESTFILTER="${1:-*}" FAILED=() SRCDIR="${SRCDIR:-$(pwd)}" +EXCLUDE_EXAMPLES=${EXCLUDED_EXAMPLES:-"wasm"} trap_errors () { @@ -29,7 +30,7 @@ trap exit 1 INT run_examples () { local examples example cd "${SRCDIR}/examples" || exit 1 - examples=$(find . -mindepth 1 -maxdepth 1 -type d -name "$TESTFILTER" | sort) + examples=$(find . -mindepth 1 -maxdepth 1 -type d -name "$TESTFILTER" | grep -vE "${EXCLUDE_EXAMPLES}" | sort) for example in $examples; do pushd "$example" > /dev/null || return 1 ./verify.sh diff --git a/ci/windows_ci_steps.sh b/ci/windows_ci_steps.sh index b6008fee888f..ff77a9ea1465 100755 --- a/ci/windows_ci_steps.sh +++ b/ci/windows_ci_steps.sh @@ -47,6 +47,7 @@ BAZEL_BUILD_OPTIONS=( -c opt --show_task_finish --verbose_failures + --define "wasm=disabled" "--test_output=errors" "${BAZEL_BUILD_EXTRA_OPTIONS[@]}" "${BAZEL_EXTRA_TEST_OPTIONS[@]}") diff --git a/docs/root/api-v3/bootstrap/bootstrap.rst b/docs/root/api-v3/bootstrap/bootstrap.rst index d2397a9bf2ac..51d7b817c66d 100644 --- a/docs/root/api-v3/bootstrap/bootstrap.rst +++ b/docs/root/api-v3/bootstrap/bootstrap.rst @@ -10,3 +10,4 @@ Bootstrap ../config/metrics/v3/metrics_service.proto ../config/overload/v3/overload.proto ../config/ratelimit/v3/rls.proto + ../extensions/wasm/v3/wasm.proto diff --git a/docs/root/api-v3/config/wasm/wasm.rst b/docs/root/api-v3/config/wasm/wasm.rst index efdb96212478..a2f03f3304bb 100644 --- a/docs/root/api-v3/config/wasm/wasm.rst +++ b/docs/root/api-v3/config/wasm/wasm.rst @@ -6,3 +6,4 @@ WASM :maxdepth: 2 ../../extensions/wasm/v3/* + ../../extensions/stat_sinks/wasm/v3/* diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index e2f5838f2c78..801d9f131943 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -40,6 +40,7 @@ HTTP filters router_filter squash_filter tap_filter + wasm_filter .. TODO(toddmgreer): Remove this hack and add user-visible CacheFilter docs when CacheFilter is production-ready. .. toctree:: diff --git a/docs/root/configuration/http/http_filters/wasm_filter.rst b/docs/root/configuration/http/http_filters/wasm_filter.rst new file mode 100644 index 000000000000..89c6528a5392 --- /dev/null +++ b/docs/root/configuration/http/http_filters/wasm_filter.rst @@ -0,0 +1,36 @@ +.. _config_http_filters_wasm: + +Wasm +==== + +* :ref:`v3 API reference ` + +.. attention:: + + The Wasm filter is experimental and is currently under active development. Capabilities will + be expanded over time and the configuration structures are likely to change. + +The HTTP Wasm filter is used implement an HTTP filter with a Wasm plugin. + +Example configuration +--------------------- + +Example filter configuration: + +.. code-block:: yaml + + name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + config: + config: + name: "my_plugin" + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "/etc/envoy_filter_http_wasm_example.wasm" + allow_precompiled: true + + +The preceding snippet configures a filter from a Wasm binary on local disk. diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index 4c29a385acad..f75a0f9c0e61 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -28,4 +28,5 @@ filters. thrift_proxy_filter sni_cluster_filter sni_dynamic_forward_proxy_filter + wasm_filter zookeeper_proxy_filter diff --git a/docs/root/configuration/listeners/network_filters/wasm_filter.rst b/docs/root/configuration/listeners/network_filters/wasm_filter.rst new file mode 100644 index 000000000000..c35627f00c4a --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/wasm_filter.rst @@ -0,0 +1,37 @@ +.. _config_network_filters_wasm: + +Wasm Network Filter +=============================================== + +* :ref:`v3 API reference ` + +.. attention:: + + The Wasm filter is experimental and is currently under active development. Capabilities will + be expanded over time and the configuration structures are likely to change. + +The Wasm network filter is used to implement a network filter with a Wasm plugin. + + +Example configuration +--------------------- + +Example filter configuration: + +.. code-block:: yaml + + name: envoy.filters.network.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm + config: + config: + name: "my_plugin" + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "/etc/envoy_filter_http_wasm_example.wasm" + allow_precompiled: true + + +The preceding snippet configures a filter from a Wasm binary on local disk. diff --git a/docs/root/configuration/other_features/other_features.rst b/docs/root/configuration/other_features/other_features.rst index 84d8f49483ce..ff59885d9381 100644 --- a/docs/root/configuration/other_features/other_features.rst +++ b/docs/root/configuration/other_features/other_features.rst @@ -5,3 +5,5 @@ Other features :maxdepth: 2 rate_limit + wasm + wasm_stat_sink diff --git a/docs/root/configuration/other_features/wasm.rst b/docs/root/configuration/other_features/wasm.rst new file mode 100644 index 000000000000..0026d6588693 --- /dev/null +++ b/docs/root/configuration/other_features/wasm.rst @@ -0,0 +1,24 @@ +.. _config_wasm_service: + +Wasm service +============ + +The :ref:`WasmService ` configuration specifies a +singleton or per-worker Wasm service for background or on-demand activities. + +Example plugin configuration: + +.. code-block:: yaml + + wasm: + config: + config: + name: "my_plugin" + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "/etc/envoy_filter_http_wasm_example.wasm" + singleton: true + +The preceding snippet configures a plugin singleton service from a Wasm binary on local disk. diff --git a/docs/root/configuration/other_features/wasm_stat_sink.rst b/docs/root/configuration/other_features/wasm_stat_sink.rst new file mode 100644 index 000000000000..c3231f26ea2b --- /dev/null +++ b/docs/root/configuration/other_features/wasm_stat_sink.rst @@ -0,0 +1,7 @@ +.. _config_stat_sinks_wasm: + +Wasm Stat Sink +============== + +The :ref:`WasmService ` configuration specifies a +singleton or per-worker Wasm stat sink service. diff --git a/examples/BUILD b/examples/BUILD index ce1a99d7df9f..d4c4d891ecfb 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -17,6 +17,7 @@ filegroup( exclude = [ "cache/responses.yaml", "jaeger-native-tracing/*", + "wasm/envoy.yaml", "**/*docker-compose*.yaml", ], ), diff --git a/examples/wasm/BUILD b/examples/wasm/BUILD new file mode 100644 index 000000000000..81f139c4b3e5 --- /dev/null +++ b/examples/wasm/BUILD @@ -0,0 +1,17 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load("//bazel/wasm:wasm.bzl", "wasm_cc_binary") + +licenses(["notice"]) # Apache 2 + +envoy_package() + +wasm_cc_binary( + name = "envoy_filter_http_wasm_example.wasm", + srcs = ["envoy_filter_http_wasm_example.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) diff --git a/examples/wasm/Dockerfile-proxy b/examples/wasm/Dockerfile-proxy new file mode 100644 index 000000000000..c7e135dfce3e --- /dev/null +++ b/examples/wasm/Dockerfile-proxy @@ -0,0 +1,5 @@ +FROM envoyproxy/envoy-dev:latest +COPY ./envoy.yaml /etc/envoy.yaml +COPY ./envoy_filter_http_wasm_example.wasm /etc/envoy_filter_http_wasm_example.wasm +RUN chmod go+r /etc/envoy.yaml +CMD /usr/local/bin/envoy -c /etc/envoy.yaml -l debug --service-cluster proxy diff --git a/examples/wasm/Dockerfile-web-service b/examples/wasm/Dockerfile-web-service new file mode 100644 index 000000000000..edf3810fa79b --- /dev/null +++ b/examples/wasm/Dockerfile-web-service @@ -0,0 +1 @@ +FROM solsson/http-echo diff --git a/examples/wasm/README.md b/examples/wasm/README.md new file mode 100644 index 000000000000..2922b607f9b7 --- /dev/null +++ b/examples/wasm/README.md @@ -0,0 +1,59 @@ +# Envoy WebAssembly Filter + +In this example, we show how a WebAssembly(WASM) filter can be used with the Envoy +proxy. The Envoy proxy [configuration](./envoy.yaml) includes a Webassembly filter +as documented [here](https://www.envoyproxy.io/docs/envoy/latest/). + + + + +## Quick Start + +1. `docker-compose build` +2. `docker-compose up` +3. `curl -v localhost:18000` + +Curl output should include our headers: + +``` +# curl -v localhost:8000 +* Rebuilt URL to: localhost:18000/ +* Trying 127.0.0.1... +* TCP_NODELAY set +* Connected to localhost (127.0.0.1) port 18000 (#0) +> GET / HTTP/1.1 +> Host: localhost:18000 +> User-Agent: curl/7.58.0 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-length: 13 +< content-type: text/plain +< location: envoy-wasm +< date: Tue, 09 Jul 2019 00:47:14 GMT +< server: envoy +< x-envoy-upstream-service-time: 0 +< newheader: newheadervalue +< +example body +* Connection #0 to host localhost left intact +``` + +## Build WASM Module + +Now you want to make changes to the C++ filter ([envoy_filter_http_wasm_example.cc](envoy_filter_http_wasm_example.cc)) +and build the WASM module ([envoy_filter_http_wasm_example.wasm](envoy_filter_http_wasm_example.wasm)). + +1. Build WASM module + ```shell + bazel build //examples/wasm:envoy_filter_http_wasm_example.wasm + ``` + +## Build the Envoy WASM Image + + + +For Envoy WASM runtime developers, if you want to make changes, please + +1. Follow [instructions](https://github.com/envoyproxy/envoy-wasm/blob/master/WASM.md). +2. Modify `docker-compose.yaml` to mount your own Envoy. diff --git a/examples/wasm/docker-compose.yaml b/examples/wasm/docker-compose.yaml new file mode 100644 index 000000000000..d2e16eb79848 --- /dev/null +++ b/examples/wasm/docker-compose.yaml @@ -0,0 +1,37 @@ +version: '3.7' +services: + + proxy: + build: + context: . + dockerfile: Dockerfile-proxy + volumes: + - ./envoy.yaml:/etc/envoy.yaml + - ./envoy_wasm_example.wasm:/etc/envoy_wasm_example.wasm + - ./envoy_filter_http_wasm_example.wasm:/etc/envoy_filter_http_wasm_example.wasm + # Uncomment this line if you want to use your own Envoy with WASM enabled. + #- /tmp/envoy-docker-build/envoy/source/exe/envoy:/usr/local/bin/envoy + networks: + - envoymesh + expose: + - "80" + - "8001" + ports: + - "18000:80" + - "18001:8001" + + web_service: + build: + context: . + dockerfile: Dockerfile-web-service + networks: + envoymesh: + aliases: + - web_service + expose: + - "80" + ports: + - "18080:80" + +networks: + envoymesh: {} diff --git a/examples/wasm/envoy.yaml b/examples/wasm/envoy.yaml new file mode 100644 index 000000000000..75f882cb3b39 --- /dev/null +++ b/examples/wasm/envoy.yaml @@ -0,0 +1,94 @@ +static_resources: + listeners: + - 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.config.filter.network.http_connection_manager.v2.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: web_service + http_filters: + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + value: + config: + name: "my_plugin" + root_id: "my_root_id" + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: | + {} + vm_config: + runtime: "envoy.wasm.runtime.v8" + vm_id: "my_vm_id" + code: + local: + filename: "/etc/envoy_filter_http_wasm_example.wasm" + configuration: {} + - name: envoy.filters.http.router + typed_config: {} + - name: staticreply + address: + socket_address: + address: 127.0.0.1 + port_value: 8099 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "foo\n" + http_filters: + - name: envoy.router + config: {} + clusters: + - name: web_service + connect_timeout: 0.25s + type: static + lb_policy: round_robin + load_assignment: + cluster_name: service1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8099 +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 diff --git a/examples/wasm/envoy_filter_http_wasm_example.cc b/examples/wasm/envoy_filter_http_wasm_example.cc new file mode 100644 index 000000000000..f3eabc3353a5 --- /dev/null +++ b/examples/wasm/envoy_filter_http_wasm_example.cc @@ -0,0 +1,90 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#include "proxy_wasm_intrinsics.h" + +class ExampleRootContext : public RootContext { +public: + explicit ExampleRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {} + + bool onStart(size_t) override; + bool onConfigure(size_t) override; + void onTick() override; +}; + +class ExampleContext : public Context { +public: + explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {} + + void onCreate() override; + FilterHeadersStatus onRequestHeaders(uint32_t headers, bool end_of_stream) override; + FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override; + FilterHeadersStatus onResponseHeaders(uint32_t headers, bool end_of_stream) override; + FilterDataStatus onResponseBody(size_t body_buffer_length, bool end_of_stream) override; + void onDone() override; + void onLog() override; + void onDelete() override; +}; +static RegisterContextFactory register_ExampleContext(CONTEXT_FACTORY(ExampleContext), + ROOT_FACTORY(ExampleRootContext), + "my_root_id"); + +bool ExampleRootContext::onStart(size_t) { + LOG_TRACE("onStart"); + return true; +} + +bool ExampleRootContext::onConfigure(size_t) { + LOG_TRACE("onConfigure"); + proxy_set_tick_period_milliseconds(1000); // 1 sec + return true; +} + +void ExampleRootContext::onTick() { LOG_TRACE("onTick"); } + +void ExampleContext::onCreate() { LOG_WARN(std::string("onCreate " + std::to_string(id()))); } + +FilterHeadersStatus ExampleContext::onRequestHeaders(uint32_t, bool) { + LOG_DEBUG(std::string("onRequestHeaders ") + std::to_string(id())); + auto result = getRequestHeaderPairs(); + auto pairs = result->pairs(); + LOG_INFO(std::string("headers: ") + std::to_string(pairs.size())); + for (auto& p : pairs) { + LOG_INFO(std::string(p.first) + std::string(" -> ") + std::string(p.second)); + } + return FilterHeadersStatus::Continue; +} + +FilterHeadersStatus ExampleContext::onResponseHeaders(uint32_t, bool) { + LOG_DEBUG(std::string("onResponseHeaders ") + std::to_string(id())); + auto result = getResponseHeaderPairs(); + auto pairs = result->pairs(); + LOG_INFO(std::string("headers: ") + std::to_string(pairs.size())); + for (auto& p : pairs) { + LOG_INFO(std::string(p.first) + std::string(" -> ") + std::string(p.second)); + } + addResponseHeader("newheader", "newheadervalue"); + replaceResponseHeader("location", "envoy-wasm"); + return FilterHeadersStatus::Continue; +} + +FilterDataStatus ExampleContext::onRequestBody(size_t body_buffer_length, + bool /* end_of_stream */) { + auto body = getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_buffer_length); + LOG_ERROR(std::string("onRequestBody ") + std::string(body->view())); + return FilterDataStatus::Continue; +} + +FilterDataStatus ExampleContext::onResponseBody(size_t /* body_buffer_length */, + bool /* end_of_stream */) { + setBuffer(WasmBufferType::HttpResponseBody, 0, 3, "foo"); + return FilterDataStatus::Continue; +} + +void ExampleContext::onDone() { LOG_WARN(std::string("onDone " + std::to_string(id()))); } + +void ExampleContext::onLog() { LOG_WARN(std::string("onLog " + std::to_string(id()))); } + +void ExampleContext::onDelete() { LOG_WARN(std::string("onDelete " + std::to_string(id()))); } diff --git a/examples/wasm/verify.sh b/examples/wasm/verify.sh new file mode 100755 index 000000000000..4a4f15bf496c --- /dev/null +++ b/examples/wasm/verify.sh @@ -0,0 +1,17 @@ +#!/bin/bash -e + +export NAME=wasm + +# shellcheck source=examples/verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + + +run_log "Test connection" +responds_with \ + foo \ + http://localhost:8000 + +run_log "Test header" +responds_with_header \ + "newheader: newheadervalue" \ + http://localhost:8000 diff --git a/generated_api_shadow/envoy/extensions/access_loggers/wasm/v3/wasm.proto b/generated_api_shadow/envoy/extensions/access_loggers/wasm/v3/wasm.proto index cd9db5906436..413743a203f0 100644 --- a/generated_api_shadow/envoy/extensions/access_loggers/wasm/v3/wasm.proto +++ b/generated_api_shadow/envoy/extensions/access_loggers/wasm/v3/wasm.proto @@ -12,9 +12,12 @@ option java_outer_classname = "WasmProto"; option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [[#not-implemented-hide:] +// [#protodoc-title: Wasm access log] +// [#extension: envoy.access_loggers.wasm] + // Custom configuration for an :ref:`AccessLog ` -// that calls into a WASM VM. +// that calls into a WASM VM. Configures the built-in *envoy.access_loggers.wasm* +// AccessLog. message WasmAccessLog { envoy.extensions.wasm.v3.PluginConfig config = 1; } diff --git a/generated_api_shadow/envoy/extensions/filters/http/wasm/v3/wasm.proto b/generated_api_shadow/envoy/extensions/filters/http/wasm/v3/wasm.proto index a812992a5b84..55eba141f45f 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/wasm/v3/wasm.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/wasm/v3/wasm.proto @@ -13,7 +13,10 @@ option java_outer_classname = "WasmProto"; option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [[#not-implemented-hide:] +// [#protodoc-title: Wasm] +// [#extension: envoy.filters.http.wasm] +// Wasm :ref:`configuration overview `. + message Wasm { // General Plugin configuration. envoy.extensions.wasm.v3.PluginConfig config = 1; diff --git a/generated_api_shadow/envoy/extensions/filters/network/wasm/v3/wasm.proto b/generated_api_shadow/envoy/extensions/filters/network/wasm/v3/wasm.proto index 131582762b59..0c1ac6af440e 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/wasm/v3/wasm.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/wasm/v3/wasm.proto @@ -13,7 +13,10 @@ option java_outer_classname = "WasmProto"; option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; -// [[#not-implemented-hide:] +// [#protodoc-title: Wasm] +// [#extension: envoy.filters.network.wasm] +// Wasm :ref:`configuration overview `. + message Wasm { // General Plugin configuration. envoy.extensions.wasm.v3.PluginConfig config = 1; diff --git a/generated_api_shadow/envoy/extensions/stat_sinks/wasm/v3/BUILD b/generated_api_shadow/envoy/extensions/stat_sinks/wasm/v3/BUILD new file mode 100644 index 000000000000..c37174bdefc4 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/stat_sinks/wasm/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/extensions/wasm/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/stat_sinks/wasm/v3/wasm.proto b/generated_api_shadow/envoy/extensions/stat_sinks/wasm/v3/wasm.proto new file mode 100644 index 000000000000..3fc5dae91795 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/stat_sinks/wasm/v3/wasm.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package envoy.extensions.stat_sinks.wasm.v3; + +import "envoy/extensions/wasm/v3/wasm.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.stat_sinks.wasm.v3"; +option java_outer_classname = "WasmProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Wasm] +// Wasm :ref:`configuration overview `. +// [#extension: envoy.stat_sinks.wasm] + +message Wasm { + // General Plugin configuration. + envoy.extensions.wasm.v3.PluginConfig config = 1; +} diff --git a/generated_api_shadow/envoy/extensions/wasm/v3/wasm.proto b/generated_api_shadow/envoy/extensions/wasm/v3/wasm.proto index c036603c5759..b42fb75a0bf7 100644 --- a/generated_api_shadow/envoy/extensions/wasm/v3/wasm.proto +++ b/generated_api_shadow/envoy/extensions/wasm/v3/wasm.proto @@ -16,8 +16,8 @@ option java_multiple_files = true; option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Wasm] +// [#extension: envoy.bootstrap.wasm] -// [[#not-implemented-hide:] // Configuration for a Wasm VM. // [#next-free-field: 7] message VmConfig { @@ -51,7 +51,6 @@ message VmConfig { bool nack_on_code_cache_miss = 6; } -// [[#not-implemented-hide:] // Base Configuration for Wasm Plugins e.g. filters and services. // [#next-free-field: 6] message PluginConfig { @@ -66,9 +65,9 @@ message PluginConfig { string root_id = 2; // Configuration for finding or starting VM. - oneof vm_config { - VmConfig inline_vm_config = 3; - // In the future add referential VM configurations. + oneof vm { + VmConfig vm_config = 3; + // TODO: add referential VM configurations. } // Filter/service configuration used to configure or reconfigure a plugin @@ -86,7 +85,6 @@ message PluginConfig { bool fail_open = 5; } -// [[#not-implemented-hide:] // WasmService is configured as a built-in *envoy.wasm_service* :ref:`WasmService // ` This opaque configuration will be used to create a Wasm Service. message WasmService { diff --git a/include/abi/wasm/proxy_wasm_common.h b/include/abi/wasm/proxy_wasm_common.h deleted file mode 100644 index 626e2d2838c6..000000000000 --- a/include/abi/wasm/proxy_wasm_common.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Common enumerations available to WASM modules and shared with sandbox. - */ -// NOLINT(namespace-envoy) - -#pragma once - -#include - -enum class WasmResult : uint32_t { - Ok = 0, - // The result could not be found, e.g. a provided key did not appear in a table. - NotFound = 1, - // An argument was bad, e.g. did not not conform to the required range. - BadArgument = 2, - // Data could not be serialized. - SerializationFailure = 3, - // Data could not be parsed. - ParseFailure = 4, - // A provided expression (e.g. "foo.bar") was illegal or unrecognized. - BadExpression = 5, - // A provided memory range was not legal. - InvalidMemoryAccess = 6, - // Data was requested from an empty container. - Empty = 7, - // The provided value did not match that of the stored data. - CompareAndSwapMismatch = 8, - // Returned result was unexpected, e.g. of the incorrect size. - ResultMismatch = 9, - // Internal failure: trying check logs of the surrounding system. - InternalFailure = 10, - // The connection/stream/pipe was broken/closed unexpectedly. - BrokenConnection = 11, -}; - -#define _CASE(_e) \ - case WasmResult::_e: \ - return #_e -inline std::string toString(WasmResult r) { - switch (r) { - _CASE(Ok); - _CASE(NotFound); - _CASE(BadArgument); - _CASE(SerializationFailure); - _CASE(ParseFailure); - _CASE(BadExpression); - _CASE(InvalidMemoryAccess); - _CASE(Empty); - _CASE(CompareAndSwapMismatch); - _CASE(ResultMismatch); - _CASE(InternalFailure); - _CASE(BrokenConnection); - } -} -#undef _CASE diff --git a/include/abi/wasm/proxy_wasm_exports.h b/include/abi/wasm/proxy_wasm_exports.h deleted file mode 100644 index f2536a8c51a4..000000000000 --- a/include/abi/wasm/proxy_wasm_exports.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Proxy-WASM ABI. - */ -// NOLINT(namespace-envoy) - -#pragma once - -#include -#include - -// -// ABI functions export from the VM to the host for calls from the host into the VM. -// -// These will typically be implemented by a language specific SDK which will provide an API on top -// of this ABI e.g. the C++ SDK provides a proxy_wasm_api.h implementation of the API on top of -// this ABI. -// -// The Wasm VM can only access memory in the VM. Consequently, all data must be passed as integral -// call parameters or by the host allocating memory in the VM which is then owned by the Wasm code. -// For consistency and to enable diverse Wasm languages (e.g. languages with GC), the ABI uses a -// single mechanism for allocating memory in the VM and requires that all memory allocations be -// explicitly requested by calls from the VM and that the Wasm code then owns the allocated memory. -// - -// Non-stream calls. - -/** - * Called when the VM starts by the first plugin to use the VM. - * @param root_context_id is an identifier for one or more related plugins. - * @param vm_configuration_size is the size of any configuration available via - * proxy_get_configuration during the lifetime of this call. - * @return non-zero on success and zero on failure (e.g. bad configuration). - */ -enum class WasmOnVmStartResult : uint32_t { - Ok = 0, - BadConfiguration = 1, -}; -extern "C" WasmOnVmStartResult proxy_on_vm_start(uint32_t root_context_id, - uint32_t vm_configuration_size); - -/** - * Can be called to validate a configuration (e.g. from bootstrap or xDS) both before - * proxy_on_start() to verify the VM configuration or after proxy_on_start() to verify a plugin - * configuration. - * @param root_context_id is a unique identifier for the configuration verification context. - * @param configuration_size is the size of any configuration available via - * proxy_get_configuration(). - * @return non-zero on success and zero on failure (i.e. bad configuration). - */ -enum class WasmOnValidateConfigurationResult : uint32_t { - Ok = 0, - BadConfiguration = 1, -}; -extern "C" WasmOnValidateConfigurationResult -proxy_validate_configuration(uint32_t root_context_id, uint32_t configuration_size); -/** - * Called when a plugin loads or when plugin configuration changes dynamically. - * @param root_context_id is an identifier for one or more related plugins. - * @param plugin_configuration_size is the size of any configuration available via - * proxy_get_configuration(). - * @return non-zero on success and zero on failure (e.g. bad configuration). - */ -enum class WasmOnConfigureResult : uint32_t { - Ok = 0, - BadConfiguration = 1, -} -extern "C" WasmOnConfigureResult proxy_on_configure(uint32_t root_context_id, - uint32_t plugin_configuration_size); - -// Stream calls. - -/** - * Called when a request, stream or other ephemeral context is created. - * @param context_id is an identifier of the ephemeral context. - * @param configuration_size is the size of any configuration available via - * proxy_get_configuration(). - */ -extern "C" void proxy_on_context_create(uint32_t context_id, uint32_t root_context_id); - -// Stream and Non-stream calls. - -/** - * For stream contexts, called when the stream has completed. Note: if applicable proxy_on_log() is - * called after proxy_on_done() and before proxy_on_delete(). For root contexts, proxy_on_done() is - * called when the VM is going to shutdown. - * @param context_id is an identifier the context. - * @return non-zero to indicate that this context is done. Stream contexts must return non-zero. - * Root contexts may return zero to defer the VM shutdown and the proxy_on_delete call until after a - * future proxy_done() call by the root context. - */ -enum class WasmOnDoneResult : uint32_t { - Done = 0, - NotDone = 1, -} -extern "C" WasmOnDoneResult proxy_on_done(uint32_t context_id); - -/** - * Called when the context is being deleted and will no longer receive any more calls. - * @param context_id is an identifier the context. - */ -extern "C" void proxy_on_delete(uint32_t context_id); diff --git a/include/abi/wasm/proxy_wasm_imports.h b/include/abi/wasm/proxy_wasm_imports.h deleted file mode 100644 index 634142003502..000000000000 --- a/include/abi/wasm/proxy_wasm_imports.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Proxy-WASM ABI. - */ -// NOLINT(namespace-envoy) - -#pragma once - -#include -#include - -// -// ABI functions imported from the host into the VM for calls from the VM to the host. -// - -// Configuration and Status - -/** - * Called from the VM to get any configuration. Valid only when in proxy_on_start() (where it will - * return a VM configuration), proxy_on_configure() (where it will return a plugin configuration) or - * in proxy_validate_configuration() (where it will return a VM configuration before - * proxy_on_start() has been called and a plugin configuration after). - * @param start is the offset of the first byte to retrieve. - * @param length is the number of the bytes to retrieve. If start + length exceeds the number of - * bytes available then configuration_size will be set to the number of bytes returned. - * @param configuration_ptr a pointer to a location which will be filled with either nullptr (if no - * configuration is available) or a pointer to a allocated block containing the configuration - * bytes. - * @param configuration_size a pointer to a location containing the size (or zero) of any returned - * configuration byte block. - * @return a WasmResult: OK, InvalidMemoryAccess. Note: if OK is returned *configuration_ptr may - * be nullptr. - */ -extern "C" WasmResult proxy_get_configuration((uint32_t start, uint32_t length, - const char** configuration_ptr, size_t* configuration_size); - -// Logging -// -// level: trace = 0, debug = 1, info = 2, warn = 3, error = 4, critical = 5 - -/** - * Called from the VM to log a message. - * @param level is one of trace = 0, debug = 1, info = 2, warn = 3, error = 4, critical = 5. - * @param log_message is a pointer to a message to log. - * @param log_message_size is the size of the message. Messages need not have a newline or be null - * terminated. - * @return a WasmResult: OK, InvalidMemoryAccess. - */ -enum class WasmLogLevel : uint32_t { - Trace = 0, Debug = 1, Info = 2, Warning = 3, Error = 4, Critical = 5, -} -extern "C" WasmResult proxy_log(WasmLogLevel level, const char* log_message, size_t log_message_size); - -// System - -/** - * Called from the VM by a root context after returning zero from proxy_on_done() to indicate that - * the root context is now done and the proxy_on_delete can be called and the VM shutdown and - * deleted. - * @return a WasmResult: OK, NotFound (if the caller did not previous return zero from - * proxy_on_done()). - */ -extern "C" WasmResult proxy_done(); diff --git a/include/envoy/common/platform.h b/include/envoy/common/platform.h index b18fc0995135..7357544f156f 100644 --- a/include/envoy/common/platform.h +++ b/include/envoy/common/platform.h @@ -255,7 +255,7 @@ constexpr absl::string_view null_device_path{"/dev/null"}; // Therefore, we decided to remove the Android check introduced here in // https://github.com/envoyproxy/envoy/pull/10120. If someone out there encounters problems with // this please bring up in Envoy's slack channel #envoy-udp-quic-dev. -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) #define ENVOY_MMSG_MORE 1 #else #define ENVOY_MMSG_MORE 0 diff --git a/source/common/config/datasource.h b/source/common/config/datasource.h index 4b3ccdb17ffd..c3969ee7f450 100644 --- a/source/common/config/datasource.h +++ b/source/common/config/datasource.h @@ -3,6 +3,7 @@ #include "envoy/api/api.h" #include "envoy/common/random_generator.h" #include "envoy/config/core/v3/base.pb.h" +#include "envoy/event/deferred_deletable.h" #include "envoy/init/manager.h" #include "envoy/upstream/cluster_manager.h" @@ -59,7 +60,8 @@ class LocalAsyncDataProvider { using LocalAsyncDataProviderPtr = std::unique_ptr; -class RemoteAsyncDataProvider : public Config::DataFetcher::RemoteDataFetcherCallback, +class RemoteAsyncDataProvider : public Event::DeferredDeletable, + public Config::DataFetcher::RemoteDataFetcherCallback, public Logger::Loggable { public: RemoteAsyncDataProvider(Upstream::ClusterManager& cm, Init::Manager& manager, diff --git a/source/common/network/BUILD b/source/common/network/BUILD index 94d2d2051932..a622f082b85b 100644 --- a/source/common/network/BUILD +++ b/source/common/network/BUILD @@ -261,6 +261,7 @@ envoy_cc_library( ], deps = [ ":address_lib", + ":default_socket_interface_lib", ":listen_socket_lib", ":udp_default_writer_config", "//include/envoy/event:dispatcher_interface", diff --git a/source/extensions/access_loggers/wasm/BUILD b/source/extensions/access_loggers/wasm/BUILD new file mode 100644 index 000000000000..efb16906d78e --- /dev/null +++ b/source/extensions/access_loggers/wasm/BUILD @@ -0,0 +1,41 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +# Access log implementation that calls into a WASM VM. + +envoy_cc_library( + name = "wasm_access_log_lib", + hdrs = ["wasm_access_log_impl.h"], + deps = [ + "//include/envoy/access_log:access_log_interface", + "//source/common/http:header_map_lib", + "//source/extensions/access_loggers:well_known_names", + "//source/extensions/common/wasm:wasm_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "unknown", + status = "alpha", + deps = [ + ":wasm_access_log_lib", + "//include/envoy/registry", + "//include/envoy/server:access_log_config_interface", + "//source/common/config:datasource_lib", + "//source/common/protobuf", + "//source/extensions/access_loggers:well_known_names", + "//source/extensions/common/wasm:wasm_lib", + "@envoy_api//envoy/extensions/access_loggers/wasm/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/access_loggers/wasm/config.cc b/source/extensions/access_loggers/wasm/config.cc new file mode 100644 index 000000000000..718adb0fad93 --- /dev/null +++ b/source/extensions/access_loggers/wasm/config.cc @@ -0,0 +1,87 @@ +#include "extensions/access_loggers/wasm/config.h" + +#include "envoy/extensions/access_loggers/wasm/v3/wasm.pb.validate.h" +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +#include "common/common/logger.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/access_loggers/wasm/wasm_access_log_impl.h" +#include "extensions/access_loggers/well_known_names.h" +#include "extensions/common/wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace AccessLoggers { +namespace Wasm { + +AccessLog::InstanceSharedPtr +WasmAccessLogFactory::createAccessLogInstance(const Protobuf::Message& proto_config, + AccessLog::FilterPtr&& filter, + Server::Configuration::FactoryContext& context) { + const auto& config = MessageUtil::downcastAndValidate< + const envoy::extensions::access_loggers::wasm::v3::WasmAccessLog&>( + proto_config, context.messageValidationVisitor()); + auto access_log = + std::make_shared(config.config().root_id(), nullptr, std::move(filter)); + + // Create a base WASM to verify that the code loads before setting/cloning the for the + // individual threads. + auto plugin = std::make_shared( + config.config().name(), config.config().root_id(), config.config().vm_config().vm_id(), + config.config().vm_config().runtime(), + Common::Wasm::anyToBytes(config.config().configuration()), config.config().fail_open(), + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, context.localInfo(), + nullptr /* listener_metadata */); + + auto callback = [access_log, &context, plugin](Common::Wasm::WasmHandleSharedPtr base_wasm) { + auto tls_slot = context.threadLocal().allocateSlot(); + + // NB: the Slot set() call doesn't complete inline, so all arguments must outlive this call. + tls_slot->set( + [base_wasm, + plugin](Event::Dispatcher& dispatcher) -> std::shared_ptr { + if (!base_wasm) { + // There is no way to prevent the connection at this point. The user could choose to use + // an HTTP Wasm plugin and only handle onLog() which would correctly close the + // connection in onRequestHeaders(). + if (!plugin->fail_open_) { + ENVOY_LOG(critical, "Plugin configured to fail closed failed to load"); + } + return nullptr; + } + return std::static_pointer_cast( + Common::Wasm::getOrCreateThreadLocalWasm(base_wasm, plugin, dispatcher)); + }); + access_log->setTlsSlot(std::move(tls_slot)); + }; + + if (!Common::Wasm::createWasm( + config.config().vm_config(), plugin, context.scope().createScope(""), + context.clusterManager(), context.initManager(), context.dispatcher(), context.api(), + context.lifecycleNotifier(), remote_data_provider_, std::move(callback))) { + throw Common::Wasm::WasmException( + fmt::format("Unable to create Wasm access log {}", plugin->name_)); + } + + return access_log; +} + +ProtobufTypes::MessagePtr WasmAccessLogFactory::createEmptyConfigProto() { + return ProtobufTypes::MessagePtr{ + new envoy::extensions::access_loggers::wasm::v3::WasmAccessLog()}; +} + +std::string WasmAccessLogFactory::name() const { return AccessLogNames::get().Wasm; } + +/** + * Static registration for the wasm access log. @see RegisterFactory. + */ +REGISTER_FACTORY(WasmAccessLogFactory, + Server::Configuration::AccessLogInstanceFactory){"envoy.wasm_access_log"}; + +} // namespace Wasm +} // namespace AccessLoggers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/access_loggers/wasm/config.h b/source/extensions/access_loggers/wasm/config.h new file mode 100644 index 000000000000..2fb297108c35 --- /dev/null +++ b/source/extensions/access_loggers/wasm/config.h @@ -0,0 +1,34 @@ +#pragma once + +#include "envoy/server/access_log_config.h" + +#include "common/config/datasource.h" + +namespace Envoy { +namespace Extensions { +namespace AccessLoggers { +namespace Wasm { + +/** + * Config registration for the file access log. @see AccessLogInstanceFactory. + */ +class WasmAccessLogFactory : public Server::Configuration::AccessLogInstanceFactory, + Logger::Loggable { +public: + AccessLog::InstanceSharedPtr + createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, + Server::Configuration::FactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() const override; + +private: + absl::flat_hash_map convertJsonFormatToMap(ProtobufWkt::Struct config); + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +} // namespace Wasm +} // namespace AccessLoggers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/access_loggers/wasm/wasm_access_log_impl.h b/source/extensions/access_loggers/wasm/wasm_access_log_impl.h new file mode 100644 index 000000000000..5a7654b97bee --- /dev/null +++ b/source/extensions/access_loggers/wasm/wasm_access_log_impl.h @@ -0,0 +1,53 @@ +#pragma once + +#include "envoy/access_log/access_log.h" + +#include "common/common/logger.h" + +#include "extensions/access_loggers/well_known_names.h" +#include "extensions/common/wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace AccessLoggers { +namespace Wasm { + +using Envoy::Extensions::Common::Wasm::WasmHandle; + +class WasmAccessLog : public AccessLog::Instance { +public: + WasmAccessLog(absl::string_view root_id, ThreadLocal::SlotPtr tls_slot, + AccessLog::FilterPtr filter) + : root_id_(root_id), tls_slot_(std::move(tls_slot)), filter_(std::move(filter)) {} + void log(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap* response_headers, + const Http::ResponseTrailerMap* response_trailers, + const StreamInfo::StreamInfo& stream_info) override { + if (filter_ && request_headers && response_headers && response_trailers) { + if (!filter_->evaluate(stream_info, *request_headers, *response_headers, + *response_trailers)) { + return; + } + } + + if (tls_slot_->get()) { + tls_slot_->getTyped().wasm()->log(root_id_, request_headers, response_headers, + response_trailers, stream_info); + } + } + + void setTlsSlot(ThreadLocal::SlotPtr tls_slot) { + ASSERT(tls_slot_ == nullptr); + tls_slot_ = std::move(tls_slot); + } + +private: + std::string root_id_; + ThreadLocal::SlotPtr tls_slot_; + AccessLog::FilterPtr filter_; +}; + +} // namespace Wasm +} // namespace AccessLoggers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/access_loggers/well_known_names.h b/source/extensions/access_loggers/well_known_names.h index 56aca60de3f2..ea39ab3e400a 100644 --- a/source/extensions/access_loggers/well_known_names.h +++ b/source/extensions/access_loggers/well_known_names.h @@ -20,6 +20,8 @@ class AccessLogNameValues { const std::string HttpGrpc = "envoy.access_loggers.http_grpc"; // TCP gRPC access log const std::string TcpGrpc = "envoy.access_loggers.tcp_grpc"; + // WASM access log + const std::string Wasm = "envoy.access_loggers.wasm"; }; using AccessLogNames = ConstSingleton; diff --git a/source/extensions/bootstrap/wasm/BUILD b/source/extensions/bootstrap/wasm/BUILD new file mode 100644 index 000000000000..e23ac8fc84a2 --- /dev/null +++ b/source/extensions/bootstrap/wasm/BUILD @@ -0,0 +1,34 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +# WASM service. + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = [ + "config.h", + ], + security_posture = "unknown", + status = "alpha", + deps = [ + "//include/envoy/registry", + "//include/envoy/server:bootstrap_extension_config_interface", + "//include/envoy/server:factory_context_interface", + "//include/envoy/server:instance_interface", + "//source/common/common:assert_lib", + "//source/common/common:empty_string", + "//source/common/config:datasource_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/grpc_credentials:well_known_names", + "@envoy_api//envoy/extensions/wasm/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/bootstrap/wasm/config.cc b/source/extensions/bootstrap/wasm/config.cc new file mode 100644 index 000000000000..3cc0068b9a16 --- /dev/null +++ b/source/extensions/bootstrap/wasm/config.cc @@ -0,0 +1,88 @@ +#include "extensions/bootstrap/wasm/config.h" + +#include "envoy/registry/registry.h" +#include "envoy/server/factory_context.h" + +#include "common/common/empty_string.h" +#include "common/config/datasource.h" +#include "common/protobuf/utility.h" + +#include "extensions/common/wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace Wasm { + +static const std::string INLINE_STRING = ""; + +void WasmFactory::createWasm(const envoy::extensions::wasm::v3::WasmService& config, + Server::Configuration::ServerFactoryContext& context, + CreateWasmServiceCallback&& cb) { + auto plugin = std::make_shared( + config.config().name(), config.config().root_id(), config.config().vm_config().vm_id(), + config.config().vm_config().runtime(), + Common::Wasm::anyToBytes(config.config().configuration()), config.config().fail_open(), + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, context.localInfo(), nullptr); + + bool singleton = config.singleton(); + auto callback = [&context, singleton, plugin, cb](Common::Wasm::WasmHandleSharedPtr base_wasm) { + if (!base_wasm) { + if (plugin->fail_open_) { + ENVOY_LOG(error, "Unable to create Wasm service {}", plugin->name_); + } else { + ENVOY_LOG(critical, "Unable to create Wasm service {}", plugin->name_); + } + return; + } + if (singleton) { + // Return a Wasm VM which will be stored as a singleton by the Server. + cb(std::make_unique( + Common::Wasm::getOrCreateThreadLocalWasm(base_wasm, plugin, context.dispatcher()))); + return; + } + // Per-thread WASM VM. + // NB: the Slot set() call doesn't complete inline, so all arguments must outlive this call. + auto tls_slot = context.threadLocal().allocateSlot(); + tls_slot->set([base_wasm, plugin](Event::Dispatcher& dispatcher) { + return std::static_pointer_cast( + Common::Wasm::getOrCreateThreadLocalWasm(base_wasm, plugin, dispatcher)); + }); + cb(std::make_unique(std::move(tls_slot))); + }; + + if (!Common::Wasm::createWasm( + config.config().vm_config(), plugin, context.scope().createScope(""), + context.clusterManager(), context.initManager(), context.dispatcher(), context.api(), + context.lifecycleNotifier(), remote_data_provider_, std::move(callback))) { + // NB: throw if we get a synchronous configuration failures as this is how such failures are + // reported to xDS. + throw Common::Wasm::WasmException( + fmt::format("Unable to create Wasm service {}", plugin->name_)); + } +} + +Server::BootstrapExtensionPtr +WasmFactory::createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) { + auto typed_config = + MessageUtil::downcastAndValidate( + config, context.messageValidationContext().staticValidationVisitor()); + + auto wasm_service_extension = std::make_unique(); + createWasm(typed_config, context, + [extension = wasm_service_extension.get()](WasmServicePtr wasm) { + extension->wasm_service_ = std::move(wasm); + }); + return wasm_service_extension; +} + +// /** +// * Static registration for the wasm factory. @see RegistryFactory. +// */ +REGISTER_FACTORY(WasmFactory, Server::Configuration::BootstrapExtensionFactory); + +} // namespace Wasm +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/wasm/config.h b/source/extensions/bootstrap/wasm/config.h new file mode 100644 index 000000000000..e70306746389 --- /dev/null +++ b/source/extensions/bootstrap/wasm/config.h @@ -0,0 +1,66 @@ +#pragma once + +#include "envoy/common/pure.h" +#include "envoy/extensions/wasm/v3/wasm.pb.h" +#include "envoy/extensions/wasm/v3/wasm.pb.validate.h" +#include "envoy/server/bootstrap_extension_config.h" +#include "envoy/server/filter_config.h" +#include "envoy/server/instance.h" + +#include "common/protobuf/protobuf.h" + +#include "extensions/common/wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace Wasm { + +class WasmService { +public: + WasmService(Common::Wasm::WasmHandleSharedPtr singleton) : singleton_(std::move(singleton)) {} + WasmService(ThreadLocal::SlotPtr tls_slot) : tls_slot_(std::move(tls_slot)) {} + +private: + Common::Wasm::WasmHandleSharedPtr singleton_; + ThreadLocal::SlotPtr tls_slot_; +}; + +using WasmServicePtr = std::unique_ptr; +using CreateWasmServiceCallback = std::function; + +class WasmFactory : public Server::Configuration::BootstrapExtensionFactory, + Logger::Loggable { +public: + ~WasmFactory() override = default; + std::string name() const override { return "envoy.bootstrap.wasm"; } + void createWasm(const envoy::extensions::wasm::v3::WasmService& config, + Server::Configuration::ServerFactoryContext& context, + CreateWasmServiceCallback&& cb); + Server::BootstrapExtensionPtr + createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + +private: + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +class WasmServiceExtension : public Server::BootstrapExtension { +public: + WasmService& wasmService() { + ASSERT(wasm_service_ != nullptr); + return *wasm_service_; + } + +private: + WasmServicePtr wasm_service_; + friend class WasmFactory; +}; + +} // namespace Wasm +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/BUILD b/source/extensions/common/wasm/BUILD index e594ac846209..02eb727951c1 100644 --- a/source/extensions/common/wasm/BUILD +++ b/source/extensions/common/wasm/BUILD @@ -16,32 +16,101 @@ envoy_cc_library( ], ) +# NB: Used to break the circular dependency between wasm_lib and null_plugin_lib. envoy_cc_library( - name = "wasm_vm_interface", - hdrs = ["wasm_vm.h"], + name = "wasm_hdr", + hdrs = [ + "context.h", + "wasm.h", + "wasm_extension.h", + "wasm_state.h", + "wasm_vm.h", + ], + visibility = ["//visibility:public"], deps = [ ":well_known_names", - "//include/envoy/stats:stats_interface", - "//source/common/common:minimal_logger_lib", + "//include/envoy/http:codes_interface", + "//include/envoy/http:filter_interface", + "//include/envoy/server:lifecycle_notifier_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/config:datasource_lib", + "//source/common/singleton:const_singleton", + "//source/common/stats:stats_lib", + "//source/common/version:version_includes", + "//source/extensions/filters/common/expr:evaluator_lib", + "//source/extensions/filters/http:well_known_names", + "@com_google_cel_cpp//eval/public:activation", + "@envoy_api//envoy/extensions/wasm/v3:pkg_cc_proto", + "@proxy_wasm_cpp_host//:include", + "@proxy_wasm_cpp_sdk//:common_lib", ], ) envoy_cc_library( - name = "wasm_vm_base", - hdrs = ["wasm_vm_base.h"], + name = "wasm_interoperation_lib", + srcs = [ + "wasm_state.cc", + ], + hdrs = [ + "wasm_state.h", + ], + visibility = ["//visibility:public"], deps = [ - ":wasm_vm_interface", - "//source/common/stats:stats_lib", + "//include/envoy/stream_info:filter_state_interface", + "//source/common/protobuf", + "//source/common/singleton:const_singleton", + "@com_github_google_flatbuffers//:flatbuffers", + "@com_google_cel_cpp//eval/public:cel_value", + "@com_google_cel_cpp//tools:flatbuffers_backed_impl", ], ) envoy_cc_library( - name = "wasm_vm_lib", - srcs = ["wasm_vm.cc"], - deps = [ - ":wasm_vm_interface", - "//source/common/common:assert_lib", - "//source/extensions/common/wasm/null:null_lib", - "//source/extensions/common/wasm/v8:v8_lib", + name = "wasm_lib", + srcs = [ + "context.cc", + "foreign.cc", + "wasm.cc", + "wasm_extension.cc", + "wasm_vm.cc", ], + copts = select({ + "//bazel:windows_x86_64": [], # TODO: fix the windows ANTLR build + "//conditions:default": [ + "-DWASM_USE_CEL_PARSER", + ], + }), + visibility = ["//visibility:public"], + deps = [ + ":wasm_hdr", + ":wasm_interoperation_lib", + "//external:abseil_base", + "//external:abseil_node_hash_map", + "//include/envoy/server:lifecycle_notifier_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:enum_to_int", + "//source/common/config:remote_data_fetcher_lib", + "//source/common/http:message_lib", + "//source/common/http:utility_lib", + "//source/common/tracing:http_tracer_lib", + "//source/extensions/common/wasm/ext:declare_property_cc_proto", + "//source/extensions/common/wasm/ext:envoy_null_vm_wasm_api", + "//source/extensions/filters/common/expr:context_lib", + "@com_google_cel_cpp//eval/eval:field_access", + "@com_google_cel_cpp//eval/eval:field_backed_list_impl", + "@com_google_cel_cpp//eval/eval:field_backed_map_impl", + "@com_google_cel_cpp//eval/public:builtin_func_registrar", + "@com_google_cel_cpp//eval/public:cel_expr_builder_factory", + "@com_google_cel_cpp//eval/public:cel_value", + "@com_google_cel_cpp//eval/public:value_export_util", + "@envoy_api//envoy/extensions/wasm/v3:pkg_cc_proto", + "@proxy_wasm_cpp_host//:lib", + ] + select( + { + "//bazel:windows_x86_64": [], + "//conditions:default": [ + "@com_google_cel_cpp//parser", + ], + }, + ), ) diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc new file mode 100644 index 000000000000..4e308524d4b7 --- /dev/null +++ b/source/extensions/common/wasm/context.cc @@ -0,0 +1,1832 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "envoy/common/exception.h" +#include "envoy/extensions/wasm/v3/wasm.pb.validate.h" +#include "envoy/grpc/status.h" +#include "envoy/http/codes.h" +#include "envoy/local_info/local_info.h" +#include "envoy/network/filter.h" +#include "envoy/stats/sink.h" +#include "envoy/thread_local/thread_local.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/assert.h" +#include "common/common/empty_string.h" +#include "common/common/enum_to_int.h" +#include "common/common/logger.h" +#include "common/http/header_map_impl.h" +#include "common/http/message_impl.h" +#include "common/http/utility.h" +#include "common/tracing/http_tracer_impl.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/common/wasm/well_known_names.h" +#include "extensions/filters/common/expr/context.h" + +#include "absl/base/casts.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/node_hash_map.h" +#include "absl/strings/str_cat.h" +#include "absl/synchronization/mutex.h" +#include "eval/eval/field_access.h" +#include "eval/eval/field_backed_list_impl.h" +#include "eval/eval/field_backed_map_impl.h" +#include "eval/public/cel_value.h" +#include "openssl/bytestring.h" +#include "openssl/hmac.h" +#include "openssl/sha.h" + +using proxy_wasm::MetricType; +using proxy_wasm::Word; + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +namespace { + +using HashPolicy = envoy::config::route::v3::RouteAction::HashPolicy; + +Http::RequestTrailerMapPtr buildRequestTrailerMapFromPairs(const Pairs& pairs) { + auto map = Http::RequestTrailerMapImpl::create(); + for (auto& p : pairs) { + // Note: because of the lack of a string_view interface for addCopy and + // the lack of an interface to add an entry with an empty value and return + // the entry, there is no efficient way to prevent either a double copy + // of the value or a double lookup of the entry. + map->addCopy(Http::LowerCaseString(std::string(p.first)), std::string(p.second)); + } + return map; +} + +Http::RequestHeaderMapPtr buildRequestHeaderMapFromPairs(const Pairs& pairs) { + auto map = Http::RequestHeaderMapImpl::create(); + for (auto& p : pairs) { + // Note: because of the lack of a string_view interface for addCopy and + // the lack of an interface to add an entry with an empty value and return + // the entry, there is no efficient way to prevent either a double copy + // of the value or a double lookup of the entry. + map->addCopy(Http::LowerCaseString(std::string(p.first)), std::string(p.second)); + } + return map; +} + +template static uint32_t headerSize(const P& p) { return p ? p->size() : 0; } + +constexpr absl::string_view FailStreamResponseDetails = "wasm_fail_stream"; + +} // namespace + +// Test support. + +size_t Buffer::size() const { + if (const_buffer_instance_) { + return const_buffer_instance_->length(); + } + return proxy_wasm::BufferBase::size(); +} + +WasmResult Buffer::copyTo(WasmBase* wasm, size_t start, size_t length, uint64_t ptr_ptr, + uint64_t size_ptr) const { + if (const_buffer_instance_) { + uint64_t pointer; + auto p = wasm->allocMemory(length, &pointer); + if (!p) { + return WasmResult::InvalidMemoryAccess; + } + const_buffer_instance_->copyOut(start, length, p); + if (!wasm->wasm_vm()->setWord(ptr_ptr, Word(pointer))) { + return WasmResult::InvalidMemoryAccess; + } + if (!wasm->wasm_vm()->setWord(size_ptr, Word(length))) { + return WasmResult::InvalidMemoryAccess; + } + return WasmResult::Ok; + } + return proxy_wasm::BufferBase::copyTo(wasm, start, length, ptr_ptr, size_ptr); +} + +WasmResult Buffer::copyFrom(size_t start, size_t length, absl::string_view data) { + if (buffer_instance_) { + if (start == 0) { + if (length == 0) { + buffer_instance_->prepend(data); + return WasmResult::Ok; + } else if (length >= buffer_instance_->length()) { + buffer_instance_->drain(buffer_instance_->length()); + buffer_instance_->add(data); + return WasmResult::Ok; + } else { + return WasmResult::BadArgument; + } + } else if (start >= buffer_instance_->length()) { + buffer_instance_->add(data); + return WasmResult::Ok; + } else { + return WasmResult::BadArgument; + } + } + if (const_buffer_instance_) { // This buffer is immutable. + return WasmResult::BadArgument; + } + return proxy_wasm::BufferBase::copyFrom(start, length, data); +} + +Context::Context() = default; +Context::Context(Wasm* wasm) : ContextBase(wasm) {} +Context::Context(Wasm* wasm, const PluginSharedPtr& plugin) : ContextBase(wasm, plugin) { + root_local_info_ = &std::static_pointer_cast(plugin)->local_info_; +} +Context::Context(Wasm* wasm, uint32_t root_context_id, const PluginSharedPtr& plugin) + : ContextBase(wasm, root_context_id, plugin) {} + +Wasm* Context::wasm() const { return static_cast(wasm_); } +Plugin* Context::plugin() const { return static_cast(plugin_.get()); } +Context* Context::rootContext() const { return static_cast(root_context()); } +Upstream::ClusterManager& Context::clusterManager() const { return wasm()->clusterManager(); } + +void Context::error(absl::string_view message) { ENVOY_LOG(trace, message); } + +uint64_t Context::getCurrentTimeNanoseconds() { + return std::chrono::duration_cast( + wasm()->time_source_.systemTime().time_since_epoch()) + .count(); +} + +void Context::onCloseTCP() { + if (tcp_connection_closed_ || !in_vm_context_created_) { + return; + } + tcp_connection_closed_ = true; + onDone(); + onLog(); + onDelete(); +} + +void Context::onResolveDns(uint32_t token, Envoy::Network::DnsResolver::ResolutionStatus status, + std::list&& response) { + proxy_wasm::DeferAfterCallActions actions(this); + if (wasm()->isFailed() || !wasm()->on_resolve_dns_) { + return; + } + if (status != Network::DnsResolver::ResolutionStatus::Success) { + buffer_.set(""); + wasm()->on_resolve_dns_(this, id_, token, 0); + return; + } + // buffer format: + // 4 bytes number of entries = N + // N * 4 bytes TTL for each entry + // N * null-terminated addresses + uint32_t s = 4; // length + for (auto& e : response) { + s += 4; // for TTL + s += e.address_->asStringView().size() + 1; // null terminated. + } + auto buffer = std::unique_ptr(new char[s]); + char* b = buffer.get(); + uint32_t n = response.size(); + memcpy(b, &n, sizeof(uint32_t)); + b += sizeof(uint32_t); + for (auto& e : response) { + uint32_t ttl = e.ttl_.count(); + memcpy(b, &ttl, sizeof(uint32_t)); + b += sizeof(uint32_t); + }; + for (auto& e : response) { + memcpy(b, e.address_->asStringView().data(), e.address_->asStringView().size()); + b += e.address_->asStringView().size(); + *b++ = 0; + }; + buffer_.set(std::move(buffer), s); + wasm()->on_resolve_dns_(this, id_, token, s); +} + +template inline uint32_t align(uint32_t i) { + return (i + sizeof(I) - 1) & ~(sizeof(I) - 1); +} + +template inline char* align(char* p) { + return reinterpret_cast((reinterpret_cast(p) + sizeof(I) - 1) & + ~(sizeof(I) - 1)); +} + +void Context::onStatsUpdate(Envoy::Stats::MetricSnapshot& snapshot) { + proxy_wasm::DeferAfterCallActions actions(this); + if (wasm()->isFailed() || !wasm()->on_stats_update_) { + return; + } + // buffer format: + // uint32 size of block of this type + // uint32 type + // uint32 count + // uint32 length of name + // name + // 8 byte alignment padding + // 8 bytes of absolute value + // 8 bytes of delta (if appropriate, e.g. for counters) + // uint32 size of block of this type + + uint32_t counter_block_size = 3 * sizeof(uint32_t); // type of stat + uint32_t num_counters = snapshot.counters().size(); + uint32_t counter_type = 1; + + uint32_t gauge_block_size = 3 * sizeof(uint32_t); // type of stat + uint32_t num_gauges = snapshot.gauges().size(); + uint32_t gauge_type = 2; + + uint32_t n = 0; + uint64_t v = 0; + + for (const auto& counter : snapshot.counters()) { + if (counter.counter_.get().used()) { + counter_block_size += sizeof(uint32_t) + counter.counter_.get().name().size(); + counter_block_size = align(counter_block_size + 2 * sizeof(uint64_t)); + } + } + + for (const auto& gauge : snapshot.gauges()) { + if (gauge.get().used()) { + gauge_block_size += sizeof(uint32_t) + gauge.get().name().size(); + gauge_block_size += align(gauge_block_size + sizeof(uint64_t)); + } + } + + auto buffer = std::unique_ptr(new char[counter_block_size + gauge_block_size]); + char* b = buffer.get(); + + memcpy(b, &counter_block_size, sizeof(uint32_t)); + b += sizeof(uint32_t); + memcpy(b, &counter_type, sizeof(uint32_t)); + b += sizeof(uint32_t); + memcpy(b, &num_counters, sizeof(uint32_t)); + b += sizeof(uint32_t); + + for (const auto& counter : snapshot.counters()) { + if (counter.counter_.get().used()) { + n = counter.counter_.get().name().size(); + memcpy(b, &n, sizeof(uint32_t)); + b += sizeof(uint32_t); + memcpy(b, counter.counter_.get().name().data(), counter.counter_.get().name().size()); + b = align(b + counter.counter_.get().name().size()); + v = counter.counter_.get().value(); + memcpy(b, &v, sizeof(uint64_t)); + b += sizeof(uint64_t); + v = counter.delta_; + memcpy(b, &v, sizeof(uint64_t)); + b += sizeof(uint64_t); + } + } + + memcpy(b, &gauge_block_size, sizeof(uint32_t)); + b += sizeof(uint32_t); + memcpy(b, &gauge_type, sizeof(uint32_t)); + b += sizeof(uint32_t); + memcpy(b, &num_gauges, sizeof(uint32_t)); + b += sizeof(uint32_t); + + for (const auto& gauge : snapshot.gauges()) { + if (gauge.get().used()) { + n = gauge.get().name().size(); + memcpy(b, &n, sizeof(uint32_t)); + b += sizeof(uint32_t); + memcpy(b, gauge.get().name().data(), gauge.get().name().size()); + b = align(b + gauge.get().name().size()); + v = gauge.get().value(); + memcpy(b, &v, sizeof(uint64_t)); + b += sizeof(uint64_t); + } + } + buffer_.set(std::move(buffer), counter_block_size + gauge_block_size); + wasm()->on_stats_update_(this, id_, counter_block_size + gauge_block_size); +} + +// Native serializer carrying over bit representation from CEL value to the extension. +// This implementation assumes that the value type is static and known to the consumer. +WasmResult serializeValue(Filters::Common::Expr::CelValue value, std::string* result) { + using Filters::Common::Expr::CelValue; + int64_t out_int64; + uint64_t out_uint64; + double out_double; + bool out_bool; + const Protobuf::Message* out_message; + switch (value.type()) { + case CelValue::Type::kString: + result->assign(value.StringOrDie().value().data(), value.StringOrDie().value().size()); + return WasmResult::Ok; + case CelValue::Type::kBytes: + result->assign(value.BytesOrDie().value().data(), value.BytesOrDie().value().size()); + return WasmResult::Ok; + case CelValue::Type::kInt64: + out_int64 = value.Int64OrDie(); + result->assign(reinterpret_cast(&out_int64), sizeof(int64_t)); + return WasmResult::Ok; + case CelValue::Type::kUint64: + out_uint64 = value.Uint64OrDie(); + result->assign(reinterpret_cast(&out_uint64), sizeof(uint64_t)); + return WasmResult::Ok; + case CelValue::Type::kDouble: + out_double = value.DoubleOrDie(); + result->assign(reinterpret_cast(&out_double), sizeof(double)); + return WasmResult::Ok; + case CelValue::Type::kBool: + out_bool = value.BoolOrDie(); + result->assign(reinterpret_cast(&out_bool), sizeof(bool)); + return WasmResult::Ok; + case CelValue::Type::kDuration: + // Warning: loss of precision to nanoseconds + out_int64 = absl::ToInt64Nanoseconds(value.DurationOrDie()); + result->assign(reinterpret_cast(&out_int64), sizeof(int64_t)); + return WasmResult::Ok; + case CelValue::Type::kTimestamp: + // Warning: loss of precision to nanoseconds + out_int64 = absl::ToUnixNanos(value.TimestampOrDie()); + result->assign(reinterpret_cast(&out_int64), sizeof(int64_t)); + return WasmResult::Ok; + case CelValue::Type::kMessage: + out_message = value.MessageOrDie(); + result->clear(); + if (!out_message || out_message->SerializeToString(result)) { + return WasmResult::Ok; + } + return WasmResult::SerializationFailure; + case CelValue::Type::kMap: { + const auto& map = *value.MapOrDie(); + const auto& keys = *map.ListKeys(); + std::vector> pairs(map.size(), std::make_pair("", "")); + for (auto i = 0; i < map.size(); i++) { + if (serializeValue(keys[i], &pairs[i].first) != WasmResult::Ok) { + return WasmResult::SerializationFailure; + } + if (serializeValue(map[keys[i]].value(), &pairs[i].second) != WasmResult::Ok) { + return WasmResult::SerializationFailure; + } + } + auto size = proxy_wasm::exports::pairsSize(pairs); + // prevent string inlining which violates byte alignment + result->resize(std::max(size, static_cast(30))); + proxy_wasm::exports::marshalPairs(pairs, result->data()); + result->resize(size); + return WasmResult::Ok; + } + case CelValue::Type::kList: { + const auto& list = *value.ListOrDie(); + std::vector> pairs(list.size(), std::make_pair("", "")); + for (auto i = 0; i < list.size(); i++) { + if (serializeValue(list[i], &pairs[i].first) != WasmResult::Ok) { + return WasmResult::SerializationFailure; + } + } + auto size = proxy_wasm::exports::pairsSize(pairs); + // prevent string inlining which violates byte alignment + if (size < 30) { + result->reserve(30); + } + result->resize(size); + proxy_wasm::exports::marshalPairs(pairs, result->data()); + return WasmResult::Ok; + } + default: + break; + } + return WasmResult::SerializationFailure; +} + +#define PROPERTY_TOKENS(_f) \ + _f(METADATA) _f(REQUEST) _f(RESPONSE) _f(CONNECTION) _f(UPSTREAM) _f(NODE) _f(SOURCE) \ + _f(DESTINATION) _f(LISTENER_DIRECTION) _f(LISTENER_METADATA) _f(CLUSTER_NAME) \ + _f(CLUSTER_METADATA) _f(ROUTE_NAME) _f(ROUTE_METADATA) _f(PLUGIN_NAME) \ + _f(PLUGIN_ROOT_ID) _f(PLUGIN_VM_ID) _f(CONNECTION_ID) _f(FILTER_STATE) + +static inline std::string downCase(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; +} + +#define _DECLARE(_t) _t, +enum class PropertyToken { PROPERTY_TOKENS(_DECLARE) }; +#undef _DECLARE + +#define _PAIR(_t) {downCase(#_t), PropertyToken::_t}, +static absl::flat_hash_map property_tokens = {PROPERTY_TOKENS(_PAIR)}; +#undef _PAIR + +absl::optional +Context::findValue(absl::string_view name, Protobuf::Arena* arena, bool last) const { + using google::api::expr::runtime::CelValue; + + const StreamInfo::StreamInfo* info = getConstRequestStreamInfo(); + + // Convert into a dense token to enable a jump table implementation. + auto part_token = property_tokens.find(name); + if (part_token == property_tokens.end()) { + if (info) { + std::string key; + absl::StrAppend(&key, WasmStateKeyPrefix, name); + const WasmState* state; + if (info->filterState().hasData(key)) { + state = &info->filterState().getDataReadOnly(key); + } else if (info->upstreamFilterState() && + info->upstreamFilterState()->hasData(key)) { + state = &info->upstreamFilterState()->getDataReadOnly(key); + } else { + return {}; + } + return state->exprValue(arena, last); + } + return {}; + } + + switch (part_token->second) { + case PropertyToken::METADATA: + if (info) { + return CelValue::CreateMessage(&info->dynamicMetadata(), arena); + } + break; + case PropertyToken::REQUEST: + if (info) { + return CelValue::CreateMap(Protobuf::Arena::Create( + arena, *arena, request_headers_ ? request_headers_ : access_log_request_headers_, *info)); + } + break; + case PropertyToken::RESPONSE: + if (info) { + return CelValue::CreateMap(Protobuf::Arena::Create( + arena, *arena, response_headers_ ? response_headers_ : access_log_response_headers_, + response_trailers_ ? response_trailers_ : access_log_response_trailers_, *info)); + } + break; + case PropertyToken::CONNECTION: + if (info) { + return CelValue::CreateMap( + Protobuf::Arena::Create(arena, *info)); + } + break; + case PropertyToken::CONNECTION_ID: { + auto conn = getConnection(); + if (conn) { + return CelValue::CreateUint64(conn->id()); + } + break; + } + case PropertyToken::UPSTREAM: + if (info) { + return CelValue::CreateMap( + Protobuf::Arena::Create(arena, *info)); + } + break; + case PropertyToken::NODE: + if (root_local_info_) { + return CelValue::CreateMessage(&root_local_info_->node(), arena); + } else if (plugin_) { + return CelValue::CreateMessage(&plugin()->local_info_.node(), arena); + } + break; + case PropertyToken::SOURCE: + if (info) { + return CelValue::CreateMap( + Protobuf::Arena::Create(arena, *info, false)); + } + break; + case PropertyToken::DESTINATION: + if (info) { + return CelValue::CreateMap( + Protobuf::Arena::Create(arena, *info, true)); + } + break; + case PropertyToken::LISTENER_DIRECTION: + if (plugin_) { + return CelValue::CreateInt64(plugin()->direction_); + } + break; + case PropertyToken::LISTENER_METADATA: + if (plugin_) { + return CelValue::CreateMessage(plugin()->listener_metadata_, arena); + } + break; + case PropertyToken::CLUSTER_NAME: + if (info && info->upstreamHost()) { + return CelValue::CreateString(&info->upstreamHost()->cluster().name()); + } else if (info && info->routeEntry()) { + return CelValue::CreateString(&info->routeEntry()->clusterName()); + } else if (info && info->upstreamClusterInfo().has_value() && + info->upstreamClusterInfo().value()) { + return CelValue::CreateString(&info->upstreamClusterInfo().value()->name()); + } + break; + case PropertyToken::CLUSTER_METADATA: + if (info && info->upstreamHost()) { + return CelValue::CreateMessage(&info->upstreamHost()->cluster().metadata(), arena); + } + break; + case PropertyToken::ROUTE_NAME: + if (info) { + return CelValue::CreateString(&info->getRouteName()); + } + break; + case PropertyToken::ROUTE_METADATA: + if (info && info->routeEntry()) { + return CelValue::CreateMessage(&info->routeEntry()->metadata(), arena); + } + break; + case PropertyToken::PLUGIN_NAME: + if (plugin_) { + return CelValue::CreateStringView(plugin()->name_); + } + break; + case PropertyToken::PLUGIN_ROOT_ID: + return CelValue::CreateStringView(root_id()); + case PropertyToken::PLUGIN_VM_ID: + return CelValue::CreateStringView(wasm()->vm_id()); + case PropertyToken::FILTER_STATE: + return Protobuf::Arena::Create(arena, + info->filterState()) + ->Produce(arena); + } + return {}; +} + +WasmResult Context::getProperty(absl::string_view path, std::string* result) { + using google::api::expr::runtime::CelValue; + + bool first = true; + CelValue value; + Protobuf::Arena arena; + + size_t start = 0; + while (true) { + if (start >= path.size()) { + break; + } + + size_t end = path.find('\0', start); + if (end == absl::string_view::npos) { + end = start + path.size(); + } + auto part = path.substr(start, end - start); + start = end + 1; + + if (first) { + // top-level identifier + first = false; + auto top_value = findValue(part, &arena, start >= path.size()); + if (!top_value.has_value()) { + return WasmResult::NotFound; + } + value = top_value.value(); + } else if (value.IsMap()) { + auto& map = *value.MapOrDie(); + auto field = map[CelValue::CreateStringView(part)]; + if (!field.has_value()) { + return WasmResult::NotFound; + } + value = field.value(); + } else if (value.IsMessage()) { + auto msg = value.MessageOrDie(); + if (msg == nullptr) { + return WasmResult::NotFound; + } + const Protobuf::Descriptor* desc = msg->GetDescriptor(); + const Protobuf::FieldDescriptor* field_desc = desc->FindFieldByName(std::string(part)); + if (field_desc == nullptr) { + return WasmResult::NotFound; + } + if (field_desc->is_map()) { + value = CelValue::CreateMap( + Protobuf::Arena::Create( + &arena, msg, field_desc, &arena)); + } else if (field_desc->is_repeated()) { + value = CelValue::CreateList( + Protobuf::Arena::Create( + &arena, msg, field_desc, &arena)); + } else { + auto status = + google::api::expr::runtime::CreateValueFromSingleField(msg, field_desc, &arena, &value); + if (!status.ok()) { + return WasmResult::InternalFailure; + } + } + } else { + return WasmResult::NotFound; + } + } + + return serializeValue(value, result); +} + +// Header/Trailer/Metadata Maps. +Http::HeaderMap* Context::getMap(WasmHeaderMapType type) { + switch (type) { + case WasmHeaderMapType::RequestHeaders: + return request_headers_; + case WasmHeaderMapType::RequestTrailers: + return request_trailers_; + case WasmHeaderMapType::ResponseHeaders: + return response_headers_; + case WasmHeaderMapType::ResponseTrailers: + return response_trailers_; + default: + return nullptr; + } +} + +const Http::HeaderMap* Context::getConstMap(WasmHeaderMapType type) { + switch (type) { + case WasmHeaderMapType::RequestHeaders: + if (access_log_request_headers_) { + return access_log_request_headers_; + } + return request_headers_; + case WasmHeaderMapType::RequestTrailers: + return request_trailers_; + case WasmHeaderMapType::ResponseHeaders: + if (access_log_response_headers_) { + return access_log_response_headers_; + } + return response_headers_; + case WasmHeaderMapType::ResponseTrailers: + if (access_log_response_trailers_) { + return access_log_response_trailers_; + } + return response_trailers_; + case WasmHeaderMapType::GrpcReceiveInitialMetadata: + return rootContext()->grpc_receive_initial_metadata_.get(); + case WasmHeaderMapType::GrpcReceiveTrailingMetadata: + return rootContext()->grpc_receive_trailing_metadata_.get(); + case WasmHeaderMapType::HttpCallResponseHeaders: { + Envoy::Http::ResponseMessagePtr* response = rootContext()->http_call_response_; + if (response) { + return &(*response)->headers(); + } + return nullptr; + } + case WasmHeaderMapType::HttpCallResponseTrailers: { + Envoy::Http::ResponseMessagePtr* response = rootContext()->http_call_response_; + if (response) { + return (*response)->trailers(); + } + return nullptr; + } + } + NOT_REACHED_GCOVR_EXCL_LINE; +} + +WasmResult Context::addHeaderMapValue(WasmHeaderMapType type, absl::string_view key, + absl::string_view value) { + auto map = getMap(type); + if (!map) { + return WasmResult::BadArgument; + } + const Http::LowerCaseString lower_key{std::string(key)}; + map->addCopy(lower_key, std::string(value)); + return WasmResult::Ok; +} + +WasmResult Context::getHeaderMapValue(WasmHeaderMapType type, absl::string_view key, + absl::string_view* value) { + auto map = getConstMap(type); + if (!map) { + return WasmResult::BadArgument; + } + const Http::LowerCaseString lower_key{std::string(key)}; + auto entry = map->get(lower_key); + if (!entry) { + if (wasm()->abiVersion() == proxy_wasm::AbiVersion::ProxyWasm_0_1_0) { + *value = ""; + return WasmResult::Ok; + } else { + return WasmResult::NotFound; + } + } + *value = entry->value().getStringView(); + return WasmResult::Ok; +} + +Pairs headerMapToPairs(const Http::HeaderMap* map) { + if (!map) { + return {}; + } + Pairs pairs; + pairs.reserve(map->size()); + map->iterate([&pairs](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { + pairs.push_back(std::make_pair(header.key().getStringView(), header.value().getStringView())); + return Http::HeaderMap::Iterate::Continue; + }); + return pairs; +} + +WasmResult Context::getHeaderMapPairs(WasmHeaderMapType type, Pairs* result) { + *result = headerMapToPairs(getConstMap(type)); + return WasmResult::Ok; +} + +WasmResult Context::setHeaderMapPairs(WasmHeaderMapType type, const Pairs& pairs) { + auto map = getMap(type); + if (!map) { + return WasmResult::BadArgument; + } + std::vector keys; + map->iterate([&keys](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { + keys.push_back(std::string(header.key().getStringView())); + return Http::HeaderMap::Iterate::Continue; + }); + for (auto& k : keys) { + const Http::LowerCaseString lower_key{k}; + map->remove(lower_key); + } + for (auto& p : pairs) { + const Http::LowerCaseString lower_key{std::string(p.first)}; + map->addCopy(lower_key, std::string(p.second)); + } + return WasmResult::Ok; +} + +WasmResult Context::removeHeaderMapValue(WasmHeaderMapType type, absl::string_view key) { + auto map = getMap(type); + if (!map) { + return WasmResult::BadArgument; + } + const Http::LowerCaseString lower_key{std::string(key)}; + map->remove(lower_key); + return WasmResult::Ok; +} + +WasmResult Context::replaceHeaderMapValue(WasmHeaderMapType type, absl::string_view key, + absl::string_view value) { + auto map = getMap(type); + if (!map) { + return WasmResult::BadArgument; + } + const Http::LowerCaseString lower_key{std::string(key)}; + map->setCopy(lower_key, value); + return WasmResult::Ok; +} + +WasmResult Context::getHeaderMapSize(WasmHeaderMapType type, uint32_t* result) { + auto map = getMap(type); + if (!map) { + return WasmResult::BadArgument; + } + *result = map->byteSize(); + return WasmResult::Ok; +} + +// Buffer + +BufferInterface* Context::getBuffer(WasmBufferType type) { + Envoy::Http::ResponseMessagePtr* response = nullptr; + switch (type) { + case WasmBufferType::CallData: + // Set before the call. + return &buffer_; + case WasmBufferType::VmConfiguration: + return buffer_.set(wasm()->vm_configuration()); + case WasmBufferType::PluginConfiguration: + if (plugin_) { + return buffer_.set(plugin_->plugin_configuration_); + } + return nullptr; + case WasmBufferType::HttpRequestBody: + if (buffering_request_body_) { + // We need the mutable version, so capture it using a callback. + // TODO: consider adding a mutableDecodingBuffer() interface. + ::Envoy::Buffer::Instance* buffer_instance{}; + decoder_callbacks_->modifyDecodingBuffer( + [&buffer_instance](::Envoy::Buffer::Instance& buffer) { buffer_instance = &buffer; }); + return buffer_.set(buffer_instance); + } + return buffer_.set(request_body_buffer_); + case WasmBufferType::HttpResponseBody: + if (buffering_response_body_) { + // TODO: consider adding a mutableDecodingBuffer() interface. + ::Envoy::Buffer::Instance* buffer_instance{}; + encoder_callbacks_->modifyEncodingBuffer( + [&buffer_instance](::Envoy::Buffer::Instance& buffer) { buffer_instance = &buffer; }); + return buffer_.set(buffer_instance); + } + return buffer_.set(response_body_buffer_); + case WasmBufferType::NetworkDownstreamData: + return buffer_.set(network_downstream_data_buffer_); + case WasmBufferType::NetworkUpstreamData: + return buffer_.set(network_upstream_data_buffer_); + case WasmBufferType::HttpCallResponseBody: + response = rootContext()->http_call_response_; + if (response) { + auto& body = (*response)->body(); + return buffer_.set(absl::string_view(static_cast(body.linearize(body.length())), + body.length())); + } + return nullptr; + case WasmBufferType::GrpcReceiveBuffer: + return buffer_.set(rootContext()->grpc_receive_buffer_.get()); + default: + return nullptr; + } +} + +void Context::onDownstreamConnectionClose(CloseType close_type) { + ContextBase::onDownstreamConnectionClose(close_type); + downstream_closed_ = true; + // Call close on TCP connection, if upstream connection closed or there was a failure seen in + // this connection. + if (upstream_closed_ || getRequestStreamInfo()->hasAnyResponseFlag()) { + onCloseTCP(); + } +} + +void Context::onUpstreamConnectionClose(CloseType close_type) { + ContextBase::onUpstreamConnectionClose(close_type); + upstream_closed_ = true; + if (downstream_closed_) { + onCloseTCP(); + } +} + +uint32_t Context::nextHttpCallToken() { + uint32_t token = next_http_call_token_++; + // Handle rollover. + for (;;) { + if (token == 0) { + token = next_http_call_token_++; + } + if (!http_request_.count(token)) { + break; + } + token = next_http_call_token_++; + } + return token; +} + +// Async call via HTTP +WasmResult Context::httpCall(absl::string_view cluster, const Pairs& request_headers, + absl::string_view request_body, const Pairs& request_trailers, + int timeout_milliseconds, uint32_t* token_ptr) { + if (timeout_milliseconds < 0) { + return WasmResult::BadArgument; + } + auto cluster_string = std::string(cluster); + if (clusterManager().get(cluster_string) == nullptr) { + return WasmResult::BadArgument; + } + + Http::RequestMessagePtr message( + new Http::RequestMessageImpl(buildRequestHeaderMapFromPairs(request_headers))); + + // Check that we were provided certain headers. + if (message->headers().Path() == nullptr || message->headers().Method() == nullptr || + message->headers().Host() == nullptr) { + return WasmResult::BadArgument; + } + + if (!request_body.empty()) { + message->body().add(request_body); + message->headers().setContentLength(request_body.size()); + } + + if (!request_trailers.empty()) { + message->trailers(buildRequestTrailerMapFromPairs(request_trailers)); + } + + absl::optional timeout; + if (timeout_milliseconds > 0) { + timeout = std::chrono::milliseconds(timeout_milliseconds); + } + + uint32_t token = nextHttpCallToken(); + auto& handler = http_request_[token]; + + // set default hash policy to be based on :authority to enable consistent hash + Http::AsyncClient::RequestOptions options; + options.setTimeout(timeout); + Protobuf::RepeatedPtrField hash_policy; + hash_policy.Add()->mutable_header()->set_header_name(Http::Headers::get().Host.get()); + options.setHashPolicy(hash_policy); + auto http_request = clusterManager() + .httpAsyncClientForCluster(cluster_string) + .send(std::move(message), handler, options); + if (!http_request) { + http_request_.erase(token); + return WasmResult::InternalFailure; + } + handler.context_ = this; + handler.token_ = token; + handler.request_ = http_request; + *token_ptr = token; + return WasmResult::Ok; +} + +uint32_t Context::nextGrpcCallToken() { + uint32_t token = next_grpc_token_++; + if (isGrpcStreamToken(token)) { + token = next_grpc_token_++; + } + // Handle rollover. Note: token is always odd. + for (;;) { + if (!grpc_call_request_.count(token)) { + break; + } + next_grpc_token_++; // Skip stream token. + token = next_grpc_token_++; + } + return token; +} + +WasmResult Context::grpcCall(absl::string_view grpc_service, absl::string_view service_name, + absl::string_view method_name, const Pairs& initial_metadata, + absl::string_view request, std::chrono::milliseconds timeout, + uint32_t* token_ptr) { + GrpcService service_proto; + if (!service_proto.ParseFromArray(grpc_service.data(), grpc_service.size())) { + return WasmResult::ParseFailure; + } + uint32_t token = nextGrpcCallToken(); + auto& handler = grpc_call_request_[token]; + handler.context_ = this; + handler.token_ = token; + auto grpc_client = + clusterManager() + .grpcAsyncClientManager() + .factoryForGrpcService(service_proto, *wasm()->scope_, true /* skip_cluster_check */) + ->create(); + grpc_initial_metadata_ = buildRequestHeaderMapFromPairs(initial_metadata); + + // set default hash policy to be based on :authority to enable consistent hash + Http::AsyncClient::RequestOptions options; + options.setTimeout(timeout); + Protobuf::RepeatedPtrField hash_policy; + hash_policy.Add()->mutable_header()->set_header_name(Http::Headers::get().Host.get()); + options.setHashPolicy(hash_policy); + + auto grpc_request = grpc_client->sendRaw(service_name, method_name, + std::make_unique<::Envoy::Buffer::OwnedImpl>(request), + handler, Tracing::NullSpan::instance(), options); + if (!grpc_request) { + grpc_call_request_.erase(token); + return WasmResult::InternalFailure; + } + handler.client_ = std::move(grpc_client); + handler.request_ = grpc_request; + *token_ptr = token; + return WasmResult::Ok; +} + +uint32_t Context::nextGrpcStreamToken() { + uint32_t token = next_grpc_token_++; + if (isGrpcCallToken(token)) { + token = next_grpc_token_++; + } + // Handle rollover. Note: token is always even. + for (;;) { + if (token == 0) { + next_grpc_token_++; // Skip call token. + token = next_grpc_token_++; + } + if (!grpc_stream_.count(token)) { + break; + } + next_grpc_token_++; // Skip call token. + token = next_grpc_token_++; + } + return token; +} + +WasmResult Context::grpcStream(absl::string_view grpc_service, absl::string_view service_name, + absl::string_view method_name, const Pairs& initial_metadata, + uint32_t* token_ptr) { + GrpcService service_proto; + if (!service_proto.ParseFromArray(grpc_service.data(), grpc_service.size())) { + return WasmResult::ParseFailure; + } + uint32_t token = nextGrpcStreamToken(); + auto& handler = grpc_stream_[token]; + handler.context_ = this; + handler.token_ = token; + auto grpc_client = + clusterManager() + .grpcAsyncClientManager() + .factoryForGrpcService(service_proto, *wasm()->scope_, true /* skip_cluster_check */) + ->create(); + grpc_initial_metadata_ = buildRequestHeaderMapFromPairs(initial_metadata); + + // set default hash policy to be based on :authority to enable consistent hash + Http::AsyncClient::StreamOptions options; + Protobuf::RepeatedPtrField hash_policy; + hash_policy.Add()->mutable_header()->set_header_name(Http::Headers::get().Host.get()); + options.setHashPolicy(hash_policy); + + auto grpc_stream = grpc_client->startRaw(service_name, method_name, handler, options); + if (!grpc_stream) { + grpc_stream_.erase(token); + return WasmResult::InternalFailure; + } + handler.client_ = std::move(grpc_client); + handler.stream_ = grpc_stream; + *token_ptr = token; + return WasmResult::Ok; +} + +// NB: this is currently called inline, so the token is known to be that of the currently +// executing grpcCall or grpcStream. +void Context::onGrpcCreateInitialMetadata(uint32_t /* token */, + Http::RequestHeaderMap& initial_metadata) { + if (grpc_initial_metadata_) { + initial_metadata = std::move(*grpc_initial_metadata_); + grpc_initial_metadata_.reset(); + } +} + +// StreamInfo +const StreamInfo::StreamInfo* Context::getConstRequestStreamInfo() const { + if (encoder_callbacks_) { + return &encoder_callbacks_->streamInfo(); + } else if (decoder_callbacks_) { + return &decoder_callbacks_->streamInfo(); + } else if (access_log_stream_info_) { + return access_log_stream_info_; + } else if (network_read_filter_callbacks_) { + return &network_read_filter_callbacks_->connection().streamInfo(); + } else if (network_write_filter_callbacks_) { + return &network_write_filter_callbacks_->connection().streamInfo(); + } + return nullptr; +} + +StreamInfo::StreamInfo* Context::getRequestStreamInfo() const { + if (encoder_callbacks_) { + return &encoder_callbacks_->streamInfo(); + } else if (decoder_callbacks_) { + return &decoder_callbacks_->streamInfo(); + } else if (network_read_filter_callbacks_) { + return &network_read_filter_callbacks_->connection().streamInfo(); + } else if (network_write_filter_callbacks_) { + return &network_write_filter_callbacks_->connection().streamInfo(); + } + return nullptr; +} + +const Network::Connection* Context::getConnection() const { + if (encoder_callbacks_) { + return encoder_callbacks_->connection(); + } else if (decoder_callbacks_) { + return decoder_callbacks_->connection(); + } else if (network_read_filter_callbacks_) { + return &network_read_filter_callbacks_->connection(); + } else if (network_write_filter_callbacks_) { + return &network_write_filter_callbacks_->connection(); + } + return nullptr; +} + +WasmResult Context::setProperty(absl::string_view path, absl::string_view value) { + auto* stream_info = getRequestStreamInfo(); + if (!stream_info) { + return WasmResult::NotFound; + } + std::string key; + absl::StrAppend(&key, WasmStateKeyPrefix, path); + WasmState* state; + if (stream_info->filterState()->hasData(key)) { + state = &stream_info->filterState()->getDataMutable(key); + } else { + const auto& it = rootContext()->state_prototypes_.find(path); + const WasmStatePrototype& prototype = it == rootContext()->state_prototypes_.end() + ? DefaultWasmStatePrototype::get() + : *it->second.get(); // NOLINT + auto state_ptr = std::make_unique(prototype); + state = state_ptr.get(); + stream_info->filterState()->setData(key, std::move(state_ptr), + StreamInfo::FilterState::StateType::Mutable, + prototype.life_span_); + } + if (!state->setValue(value)) { + return WasmResult::BadArgument; + } + return WasmResult::Ok; +} + +WasmResult Context::declareProperty(absl::string_view path, + std::unique_ptr state_prototype) { + // Do not delete existing schema since it can be referenced by state objects. + if (state_prototypes_.find(path) == state_prototypes_.end()) { + state_prototypes_[path] = std::move(state_prototype); + return WasmResult::Ok; + } + return WasmResult::BadArgument; +} + +WasmResult Context::log(uint32_t level, absl::string_view message) { + switch (static_cast(level)) { + case spdlog::level::trace: + ENVOY_LOG(trace, "wasm log{}: {}", log_prefix(), message); + return WasmResult::Ok; + case spdlog::level::debug: + ENVOY_LOG(debug, "wasm log{}: {}", log_prefix(), message); + return WasmResult::Ok; + case spdlog::level::info: + ENVOY_LOG(info, "wasm log{}: {}", log_prefix(), message); + return WasmResult::Ok; + case spdlog::level::warn: + ENVOY_LOG(warn, "wasm log{}: {}", log_prefix(), message); + return WasmResult::Ok; + case spdlog::level::err: + ENVOY_LOG(error, "wasm log{}: {}", log_prefix(), message); + return WasmResult::Ok; + case spdlog::level::critical: + ENVOY_LOG(critical, "wasm log{}: {}", log_prefix(), message); + return WasmResult::Ok; + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +uint32_t Context::getLogLevel() { + // Like the "log" call above, assume that spdlog level as an int + // matches the enum in the SDK + return static_cast(ENVOY_LOGGER().level()); +} + +// +// Calls into the Wasm code. +// +bool Context::validateConfiguration(absl::string_view configuration, + const std::shared_ptr& plugin_base) { + auto plugin = std::static_pointer_cast(plugin_base); + if (!wasm()->validate_configuration_) { + return true; + } + plugin_ = plugin_base; + auto result = + wasm() + ->validate_configuration_(this, id_, static_cast(configuration.size())) + .u64_ != 0; + plugin_.reset(); + return result; +} + +absl::string_view Context::getConfiguration() { + if (plugin_) { + return plugin_->plugin_configuration_; + } else { + return wasm()->vm_configuration(); + } +}; + +std::pair Context::getStatus() { + return std::make_pair(status_code_, status_message_); +} + +void Context::onGrpcReceiveInitialMetadataWrapper(uint32_t token, Http::HeaderMapPtr&& metadata) { + grpc_receive_initial_metadata_ = std::move(metadata); + onGrpcReceiveInitialMetadata(token, headerSize(grpc_receive_initial_metadata_)); + grpc_receive_initial_metadata_ = nullptr; +} + +void Context::onGrpcReceiveTrailingMetadataWrapper(uint32_t token, Http::HeaderMapPtr&& metadata) { + grpc_receive_trailing_metadata_ = std::move(metadata); + onGrpcReceiveTrailingMetadata(token, headerSize(grpc_receive_trailing_metadata_)); + grpc_receive_trailing_metadata_ = nullptr; +} + +WasmResult Context::defineMetric(uint32_t metric_type, absl::string_view name, + uint32_t* metric_id_ptr) { + if (metric_type > static_cast(MetricType::Max)) { + return WasmResult::BadArgument; + } + auto type = static_cast(metric_type); + // TODO: Consider rethinking the scoping policy as it does not help in this case. + Stats::StatNameManagedStorage storage(name, wasm()->scope_->symbolTable()); + Stats::StatName stat_name = storage.statName(); + if (type == MetricType::Counter) { + auto id = wasm()->nextCounterMetricId(); + auto c = &wasm()->scope_->counterFromStatName(stat_name); + wasm()->counters_.emplace(id, c); + *metric_id_ptr = id; + return WasmResult::Ok; + } + if (type == MetricType::Gauge) { + auto id = wasm()->nextGaugeMetricId(); + auto g = &wasm()->scope_->gaugeFromStatName(stat_name, Stats::Gauge::ImportMode::Accumulate); + wasm()->gauges_.emplace(id, g); + *metric_id_ptr = id; + return WasmResult::Ok; + } + // (type == MetricType::Histogram) { + auto id = wasm()->nextHistogramMetricId(); + auto h = &wasm()->scope_->histogramFromStatName(stat_name, Stats::Histogram::Unit::Unspecified); + wasm()->histograms_.emplace(id, h); + *metric_id_ptr = id; + return WasmResult::Ok; +} + +WasmResult Context::incrementMetric(uint32_t metric_id, int64_t offset) { + auto type = static_cast(metric_id & Wasm::kMetricTypeMask); + if (type == MetricType::Counter) { + auto it = wasm()->counters_.find(metric_id); + if (it != wasm()->counters_.end()) { + if (offset > 0) { + it->second->add(offset); + return WasmResult::Ok; + } else { + return WasmResult::BadArgument; + } + } + return WasmResult::NotFound; + } else if (type == MetricType::Gauge) { + auto it = wasm()->gauges_.find(metric_id); + if (it != wasm()->gauges_.end()) { + if (offset > 0) { + it->second->add(offset); + return WasmResult::Ok; + } else { + it->second->sub(-offset); + return WasmResult::Ok; + } + } + return WasmResult::NotFound; + } + return WasmResult::BadArgument; +} + +WasmResult Context::recordMetric(uint32_t metric_id, uint64_t value) { + auto type = static_cast(metric_id & Wasm::kMetricTypeMask); + if (type == MetricType::Counter) { + auto it = wasm()->counters_.find(metric_id); + if (it != wasm()->counters_.end()) { + it->second->add(value); + return WasmResult::Ok; + } + } else if (type == MetricType::Gauge) { + auto it = wasm()->gauges_.find(metric_id); + if (it != wasm()->gauges_.end()) { + it->second->set(value); + return WasmResult::Ok; + } + } else if (type == MetricType::Histogram) { + auto it = wasm()->histograms_.find(metric_id); + if (it != wasm()->histograms_.end()) { + it->second->recordValue(value); + return WasmResult::Ok; + } + } + return WasmResult::NotFound; +} + +WasmResult Context::getMetric(uint32_t metric_id, uint64_t* result_uint64_ptr) { + auto type = static_cast(metric_id & Wasm::kMetricTypeMask); + if (type == MetricType::Counter) { + auto it = wasm()->counters_.find(metric_id); + if (it != wasm()->counters_.end()) { + *result_uint64_ptr = it->second->value(); + return WasmResult::Ok; + } + return WasmResult::NotFound; + } else if (type == MetricType::Gauge) { + auto it = wasm()->gauges_.find(metric_id); + if (it != wasm()->gauges_.end()) { + *result_uint64_ptr = it->second->value(); + return WasmResult::Ok; + } + return WasmResult::NotFound; + } + return WasmResult::BadArgument; +} + +Context::~Context() { + // Cancel any outstanding requests. + for (auto& p : http_request_) { + p.second.request_->cancel(); + } + for (auto& p : grpc_call_request_) { + p.second.request_->cancel(); + } + for (auto& p : grpc_stream_) { + p.second.stream_->resetStream(); + } +} + +Network::FilterStatus convertNetworkFilterStatus(proxy_wasm::FilterStatus status) { + switch (status) { + default: + case proxy_wasm::FilterStatus::Continue: + return Network::FilterStatus::Continue; + case proxy_wasm::FilterStatus::StopIteration: + return Network::FilterStatus::StopIteration; + } +}; + +Http::FilterHeadersStatus convertFilterHeadersStatus(proxy_wasm::FilterHeadersStatus status) { + switch (status) { + default: + case proxy_wasm::FilterHeadersStatus::Continue: + return Http::FilterHeadersStatus::Continue; + case proxy_wasm::FilterHeadersStatus::StopIteration: + return Http::FilterHeadersStatus::StopIteration; + case proxy_wasm::FilterHeadersStatus::StopAllIterationAndBuffer: + return Http::FilterHeadersStatus::StopAllIterationAndBuffer; + case proxy_wasm::FilterHeadersStatus::StopAllIterationAndWatermark: + return Http::FilterHeadersStatus::StopAllIterationAndWatermark; + } +}; + +Http::FilterTrailersStatus convertFilterTrailersStatus(proxy_wasm::FilterTrailersStatus status) { + switch (status) { + default: + case proxy_wasm::FilterTrailersStatus::Continue: + return Http::FilterTrailersStatus::Continue; + case proxy_wasm::FilterTrailersStatus::StopIteration: + return Http::FilterTrailersStatus::StopIteration; + } +}; + +Http::FilterMetadataStatus convertFilterMetadataStatus(proxy_wasm::FilterMetadataStatus status) { + switch (status) { + default: + case proxy_wasm::FilterMetadataStatus::Continue: + return Http::FilterMetadataStatus::Continue; + } +}; + +Http::FilterDataStatus convertFilterDataStatus(proxy_wasm::FilterDataStatus status) { + switch (status) { + default: + case proxy_wasm::FilterDataStatus::Continue: + return Http::FilterDataStatus::Continue; + case proxy_wasm::FilterDataStatus::StopIterationAndBuffer: + return Http::FilterDataStatus::StopIterationAndBuffer; + case proxy_wasm::FilterDataStatus::StopIterationAndWatermark: + return Http::FilterDataStatus::StopIterationAndWatermark; + case proxy_wasm::FilterDataStatus::StopIterationNoBuffer: + return Http::FilterDataStatus::StopIterationNoBuffer; + } +}; + +Network::FilterStatus Context::onNewConnection() { + onCreate(); + return convertNetworkFilterStatus(onNetworkNewConnection()); +}; + +Network::FilterStatus Context::onData(::Envoy::Buffer::Instance& data, bool end_stream) { + if (!in_vm_context_created_) { + return Network::FilterStatus::Continue; + } + network_downstream_data_buffer_ = &data; + end_of_stream_ = end_stream; + auto result = convertNetworkFilterStatus(onDownstreamData(data.length(), end_stream)); + if (result == Network::FilterStatus::Continue) { + network_downstream_data_buffer_ = nullptr; + } + return result; +} + +Network::FilterStatus Context::onWrite(::Envoy::Buffer::Instance& data, bool end_stream) { + if (!in_vm_context_created_) { + return Network::FilterStatus::Continue; + } + network_upstream_data_buffer_ = &data; + end_of_stream_ = end_stream; + auto result = convertNetworkFilterStatus(onUpstreamData(data.length(), end_stream)); + if (result == Network::FilterStatus::Continue) { + network_upstream_data_buffer_ = nullptr; + } + if (end_stream) { + // This is called when seeing end_stream=true and not on an upstream connection event, + // because registering for latter requires replicating the whole TCP proxy extension. + onUpstreamConnectionClose(CloseType::Unknown); + } + return result; +} + +void Context::onEvent(Network::ConnectionEvent event) { + if (!in_vm_context_created_) { + return; + } + switch (event) { + case Network::ConnectionEvent::LocalClose: + onDownstreamConnectionClose(CloseType::Local); + break; + case Network::ConnectionEvent::RemoteClose: + onDownstreamConnectionClose(CloseType::Remote); + break; + default: + break; + } +} + +void Context::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) { + network_read_filter_callbacks_ = &callbacks; + network_read_filter_callbacks_->connection().addConnectionCallbacks(*this); +} + +void Context::initializeWriteFilterCallbacks(Network::WriteFilterCallbacks& callbacks) { + network_write_filter_callbacks_ = &callbacks; +} + +void Context::log(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap* response_headers, + const Http::ResponseTrailerMap* response_trailers, + const StreamInfo::StreamInfo& stream_info) { + if (!in_vm_context_created_) { + // If the request is invalid then onRequestHeaders() will not be called and neither will + // onCreate() in cases like sendLocalReply who short-circuits envoy + // lifecycle. This is because Envoy does not have a well defined lifetime for the combined + // HTTP + // + AccessLog filter. Thus, to log these scenarios, we call onCreate() in log function below. + onCreate(); + } + + access_log_request_headers_ = request_headers; + // ? request_trailers ? + access_log_response_headers_ = response_headers; + access_log_response_trailers_ = response_trailers; + access_log_stream_info_ = &stream_info; + + onLog(); + + access_log_request_headers_ = nullptr; + // ? request_trailers ? + access_log_response_headers_ = nullptr; + access_log_response_trailers_ = nullptr; + access_log_stream_info_ = nullptr; +} + +void Context::onDestroy() { + if (destroyed_ || !in_vm_context_created_) { + return; + } + destroyed_ = true; + onDone(); + onDelete(); +} + +WasmResult Context::continueStream(WasmStreamType stream_type) { + switch (stream_type) { + case WasmStreamType::Request: + if (decoder_callbacks_) { + decoder_callbacks_->continueDecoding(); + } + break; + case WasmStreamType::Response: + if (encoder_callbacks_) { + encoder_callbacks_->continueEncoding(); + } + break; + default: + return WasmResult::BadArgument; + } + request_headers_ = nullptr; + request_body_buffer_ = nullptr; + request_trailers_ = nullptr; + request_metadata_ = nullptr; + return WasmResult::Ok; +} + +WasmResult Context::closeStream(WasmStreamType stream_type) { + switch (stream_type) { + case WasmStreamType::Request: + if (decoder_callbacks_) { + if (!decoder_callbacks_->streamInfo().responseCodeDetails().has_value()) { + decoder_callbacks_->streamInfo().setResponseCodeDetails(FailStreamResponseDetails); + } + decoder_callbacks_->resetStream(); + } + return WasmResult::Ok; + case WasmStreamType::Response: + if (encoder_callbacks_) { + if (!encoder_callbacks_->streamInfo().responseCodeDetails().has_value()) { + encoder_callbacks_->streamInfo().setResponseCodeDetails(FailStreamResponseDetails); + } + encoder_callbacks_->resetStream(); + } + return WasmResult::Ok; + case WasmStreamType::Downstream: + if (network_read_filter_callbacks_) { + network_read_filter_callbacks_->connection().close( + Envoy::Network::ConnectionCloseType::FlushWrite); + } + return WasmResult::Ok; + case WasmStreamType::Upstream: + network_write_filter_callbacks_->connection().close( + Envoy::Network::ConnectionCloseType::FlushWrite); + return WasmResult::Ok; + } + return WasmResult::BadArgument; +} + +WasmResult Context::sendLocalResponse(uint32_t response_code, absl::string_view body_text, + Pairs additional_headers, uint32_t grpc_status, + absl::string_view details) { + // "additional_headers" is a collection of string_views. These will no longer + // be valid when "modify_headers" is finally called below, so we must + // make copies of all the headers. + std::vector> additional_headers_copy; + for (auto& p : additional_headers) { + const Http::LowerCaseString lower_key{std::string(p.first)}; + additional_headers_copy.emplace_back(lower_key, std::string(p.second)); + } + + auto modify_headers = [additional_headers_copy](Http::HeaderMap& headers) { + for (auto& p : additional_headers_copy) { + headers.addCopy(p.first, p.second); + } + }; + + if (decoder_callbacks_) { + // This is a bit subtle because proxy_on_delete() does call DeferAfterCallActions(), + // so in theory it could call this and the Context in the VM would be invalid, + // but because it only gets called after the connections have drained, the call to + // sendLocalReply() will fail. Net net, this is safe. + wasm()->addAfterVmCallAction([this, response_code, body_text = std::string(body_text), + modify_headers = std::move(modify_headers), grpc_status, + details = std::string(details)] { + decoder_callbacks_->sendLocalReply(static_cast(response_code), body_text, + modify_headers, grpc_status, details); + }); + } + return WasmResult::Ok; +} + +Http::FilterHeadersStatus Context::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { + onCreate(); + http_request_started_ = true; + request_headers_ = &headers; + end_of_stream_ = end_stream; + auto result = convertFilterHeadersStatus(onRequestHeaders(headerSize(&headers), end_stream)); + if (result == Http::FilterHeadersStatus::Continue) { + request_headers_ = nullptr; + } + return result; +} + +Http::FilterDataStatus Context::decodeData(::Envoy::Buffer::Instance& data, bool end_stream) { + if (!http_request_started_) { + return Http::FilterDataStatus::Continue; + } + request_body_buffer_ = &data; + end_of_stream_ = end_stream; + const auto buffer = getBuffer(WasmBufferType::HttpRequestBody); + const auto buffer_size = (buffer == nullptr) ? 0 : buffer->size(); + auto result = convertFilterDataStatus(onRequestBody(buffer_size, end_stream)); + buffering_request_body_ = false; + switch (result) { + case Http::FilterDataStatus::Continue: + request_body_buffer_ = nullptr; + break; + case Http::FilterDataStatus::StopIterationAndBuffer: + buffering_request_body_ = true; + break; + case Http::FilterDataStatus::StopIterationAndWatermark: + case Http::FilterDataStatus::StopIterationNoBuffer: + break; + } + return result; +} + +Http::FilterTrailersStatus Context::decodeTrailers(Http::RequestTrailerMap& trailers) { + if (!http_request_started_) { + return Http::FilterTrailersStatus::Continue; + } + request_trailers_ = &trailers; + auto result = convertFilterTrailersStatus(onRequestTrailers(headerSize(&trailers))); + if (result == Http::FilterTrailersStatus::Continue) { + request_trailers_ = nullptr; + } + return result; +} + +Http::FilterMetadataStatus Context::decodeMetadata(Http::MetadataMap& request_metadata) { + if (!http_request_started_) { + return Http::FilterMetadataStatus::Continue; + } + request_metadata_ = &request_metadata; + auto result = convertFilterMetadataStatus(onRequestMetadata(headerSize(&request_metadata))); + if (result == Http::FilterMetadataStatus::Continue) { + request_metadata_ = nullptr; + } + return result; +} + +void Context::setDecoderFilterCallbacks(Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { + decoder_callbacks_ = &callbacks; +} + +Http::FilterHeadersStatus Context::encode100ContinueHeaders(Http::ResponseHeaderMap&) { + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterHeadersStatus Context::encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) { + if (!http_request_started_) { + return Http::FilterHeadersStatus::Continue; + } + response_headers_ = &headers; + end_of_stream_ = end_stream; + auto result = convertFilterHeadersStatus(onResponseHeaders(headerSize(&headers), end_stream)); + if (result == Http::FilterHeadersStatus::Continue) { + response_headers_ = nullptr; + } + return result; +} + +Http::FilterDataStatus Context::encodeData(::Envoy::Buffer::Instance& data, bool end_stream) { + if (!http_request_started_) { + return Http::FilterDataStatus::Continue; + } + response_body_buffer_ = &data; + end_of_stream_ = end_stream; + const auto buffer = getBuffer(WasmBufferType::HttpResponseBody); + const auto buffer_size = (buffer == nullptr) ? 0 : buffer->size(); + auto result = convertFilterDataStatus(onResponseBody(buffer_size, end_stream)); + buffering_response_body_ = false; + switch (result) { + case Http::FilterDataStatus::Continue: + request_body_buffer_ = nullptr; + break; + case Http::FilterDataStatus::StopIterationAndBuffer: + buffering_response_body_ = true; + break; + case Http::FilterDataStatus::StopIterationAndWatermark: + case Http::FilterDataStatus::StopIterationNoBuffer: + break; + } + return result; +} + +Http::FilterTrailersStatus Context::encodeTrailers(Http::ResponseTrailerMap& trailers) { + if (!http_request_started_) { + return Http::FilterTrailersStatus::Continue; + } + response_trailers_ = &trailers; + auto result = convertFilterTrailersStatus(onResponseTrailers(headerSize(&trailers))); + if (result == Http::FilterTrailersStatus::Continue) { + response_trailers_ = nullptr; + } + return result; +} + +Http::FilterMetadataStatus Context::encodeMetadata(Http::MetadataMap& response_metadata) { + if (!http_request_started_) { + return Http::FilterMetadataStatus::Continue; + } + response_metadata_ = &response_metadata; + auto result = convertFilterMetadataStatus(onResponseMetadata(headerSize(&response_metadata))); + if (result == Http::FilterMetadataStatus::Continue) { + response_metadata_ = nullptr; + } + return result; +} + +// Http::FilterMetadataStatus::Continue; + +void Context::setEncoderFilterCallbacks(Envoy::Http::StreamEncoderFilterCallbacks& callbacks) { + encoder_callbacks_ = &callbacks; +} + +void Context::onHttpCallSuccess(uint32_t token, Envoy::Http::ResponseMessagePtr&& response) { + // TODO: convert this into a function in proxy-wasm-cpp-host and use here. + if (proxy_wasm::current_context_ != nullptr) { + // We are in a reentrant call, so defer. + wasm()->addAfterVmCallAction([this, token, response = response.release()] { + onHttpCallSuccess(token, std::unique_ptr(response)); + }); + return; + } + http_call_response_ = &response; + uint32_t body_size = response->body().length(); + onHttpCallResponse(token, response->headers().size(), body_size, + headerSize(response->trailers())); + http_call_response_ = nullptr; + http_request_.erase(token); +} + +void Context::onHttpCallFailure(uint32_t token, Http::AsyncClient::FailureReason reason) { + if (proxy_wasm::current_context_ != nullptr) { + // We are in a reentrant call, so defer. + wasm()->addAfterVmCallAction([this, token, reason] { onHttpCallFailure(token, reason); }); + return; + } + status_code_ = static_cast(WasmResult::BrokenConnection); + // This is the only value currently. + ASSERT(reason == Http::AsyncClient::FailureReason::Reset); + status_message_ = "reset"; + onHttpCallResponse(token, 0, 0, 0); + status_message_ = ""; + http_request_.erase(token); +} + +void Context::onGrpcReceiveWrapper(uint32_t token, ::Envoy::Buffer::InstancePtr response) { + ASSERT(proxy_wasm::current_context_ == nullptr); // Non-reentrant. + if (wasm()->on_grpc_receive_) { + grpc_receive_buffer_ = std::move(response); + uint32_t response_size = grpc_receive_buffer_->length(); + ContextBase::onGrpcReceive(token, response_size); + grpc_receive_buffer_.reset(); + } + if (isGrpcCallToken(token)) { + grpc_call_request_.erase(token); + } +} + +void Context::onGrpcCloseWrapper(uint32_t token, const Grpc::Status::GrpcStatus& status, + const absl::string_view message) { + if (proxy_wasm::current_context_ != nullptr) { + // We are in a reentrant call, so defer. + wasm()->addAfterVmCallAction([this, token, status, message = std::string(message)] { + onGrpcCloseWrapper(token, status, message); + }); + return; + } + if (wasm()->on_grpc_close_) { + status_code_ = static_cast(status); + status_message_ = message; + onGrpcClose(token, status_code_); + status_message_ = ""; + } + if (isGrpcCallToken(token)) { + grpc_call_request_.erase(token); + } else { + auto it = grpc_stream_.find(token); + if (it != grpc_stream_.end()) { + if (it->second.local_closed_) { + grpc_stream_.erase(token); + } + } + } +} + +WasmResult Context::grpcSend(uint32_t token, absl::string_view message, bool end_stream) { + if (isGrpcCallToken(token)) { + return WasmResult::BadArgument; + } + auto it = grpc_stream_.find(token); + if (it == grpc_stream_.end()) { + return WasmResult::NotFound; + } + if (it->second.stream_) { + it->second.stream_->sendMessageRaw(::Envoy::Buffer::InstancePtr(new ::Envoy::Buffer::OwnedImpl( + message.data(), message.size())), + end_stream); + } + return WasmResult::Ok; +} + +WasmResult Context::grpcClose(uint32_t token) { + if (isGrpcCallToken(token)) { + auto it = grpc_call_request_.find(token); + if (it == grpc_call_request_.end()) { + return WasmResult::NotFound; + } + if (it->second.request_) { + it->second.request_->cancel(); + } + grpc_call_request_.erase(token); + } else { + auto it = grpc_stream_.find(token); + if (it == grpc_stream_.end()) { + return WasmResult::NotFound; + } + if (it->second.stream_) { + it->second.stream_->closeStream(); + } + if (it->second.remote_closed_) { + grpc_stream_.erase(token); + } else { + it->second.local_closed_ = true; + } + } + return WasmResult::Ok; +} + +WasmResult Context::grpcCancel(uint32_t token) { + if (isGrpcCallToken(token)) { + auto it = grpc_call_request_.find(token); + if (it == grpc_call_request_.end()) { + return WasmResult::NotFound; + } + if (it->second.request_) { + it->second.request_->cancel(); + } + grpc_call_request_.erase(token); + } else { + auto it = grpc_stream_.find(token); + if (it == grpc_stream_.end()) { + return WasmResult::NotFound; + } + if (it->second.stream_) { + it->second.stream_->resetStream(); + } + grpc_stream_.erase(token); + } + return WasmResult::Ok; +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/context.h b/source/extensions/common/wasm/context.h new file mode 100644 index 000000000000..e288c1e50602 --- /dev/null +++ b/source/extensions/common/wasm/context.h @@ -0,0 +1,485 @@ +#pragma once + +#include +#include +#include + +#include "envoy/access_log/access_log.h" +#include "envoy/buffer/buffer.h" +#include "envoy/extensions/wasm/v3/wasm.pb.validate.h" +#include "envoy/http/filter.h" +#include "envoy/stats/sink.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/assert.h" +#include "common/common/logger.h" + +#include "extensions/common/wasm/wasm_state.h" +#include "extensions/filters/common/expr/evaluator.h" + +#include "eval/public/activation.h" +#include "include/proxy-wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +using proxy_wasm::BufferInterface; +using proxy_wasm::CloseType; +using proxy_wasm::ContextBase; +using proxy_wasm::Pairs; +using proxy_wasm::PairsWithStringValues; +using proxy_wasm::PluginBase; +using proxy_wasm::SharedQueueDequeueToken; +using proxy_wasm::SharedQueueEnqueueToken; +using proxy_wasm::WasmBase; +using proxy_wasm::WasmBufferType; +using proxy_wasm::WasmHandleBase; +using proxy_wasm::WasmHeaderMapType; +using proxy_wasm::WasmResult; +using proxy_wasm::WasmStreamType; + +using VmConfig = envoy::extensions::wasm::v3::VmConfig; +using GrpcService = envoy::config::core::v3::GrpcService; + +class Wasm; + +using WasmHandleBaseSharedPtr = std::shared_ptr; + +// Opaque context object. +class StorageObject { +public: + virtual ~StorageObject() = default; +}; + +class Buffer : public proxy_wasm::BufferBase { +public: + Buffer() = default; + + // proxy_wasm::BufferInterface + size_t size() const override; + WasmResult copyTo(WasmBase* wasm, size_t start, size_t length, uint64_t ptr_ptr, + uint64_t size_ptr) const override; + WasmResult copyFrom(size_t start, size_t length, absl::string_view data) override; + + // proxy_wasm::BufferBase + void clear() override { + proxy_wasm::BufferBase::clear(); + const_buffer_instance_ = nullptr; + buffer_instance_ = nullptr; + } + Buffer* set(absl::string_view data) { + return static_cast(proxy_wasm::BufferBase::set(data)); + } + Buffer* set(std::unique_ptr owned_data, uint32_t owned_data_size) { + return static_cast( + proxy_wasm::BufferBase::set(std::move(owned_data), owned_data_size)); + } + + Buffer* set(::Envoy::Buffer::Instance* buffer_instance) { + clear(); + buffer_instance_ = buffer_instance; + const_buffer_instance_ = buffer_instance; + return this; + } + Buffer* set(const ::Envoy::Buffer::Instance* buffer_instance) { + clear(); + const_buffer_instance_ = buffer_instance; + return this; + } + +private: + const ::Envoy::Buffer::Instance* const_buffer_instance_{}; + ::Envoy::Buffer::Instance* buffer_instance_{}; +}; + +// Plugin contains the information for a filter/service. +struct Plugin : public PluginBase { + Plugin(absl::string_view name, absl::string_view root_id, absl::string_view vm_id, + absl::string_view runtime, absl::string_view plugin_configuration, bool fail_open, + envoy::config::core::v3::TrafficDirection direction, + const LocalInfo::LocalInfo& local_info, + const envoy::config::core::v3::Metadata* listener_metadata) + : PluginBase(name, root_id, vm_id, runtime, plugin_configuration, fail_open), + direction_(direction), local_info_(local_info), listener_metadata_(listener_metadata) {} + + envoy::config::core::v3::TrafficDirection direction_; + const LocalInfo::LocalInfo& local_info_; + const envoy::config::core::v3::Metadata* listener_metadata_; +}; +using PluginSharedPtr = std::shared_ptr; + +// A context which will be the target of callbacks for a particular session +// e.g. a handler of a stream. +class Context : public proxy_wasm::ContextBase, + public Logger::Loggable, + public AccessLog::Instance, + public Http::StreamFilter, + public Network::ConnectionCallbacks, + public Network::Filter, + public google::api::expr::runtime::BaseActivation, + public std::enable_shared_from_this { +public: + Context(); // Testing. + Context(Wasm* wasm); // Vm Context. + Context(Wasm* wasm, const PluginSharedPtr& plugin); // Root Context. + Context(Wasm* wasm, uint32_t root_context_id, const PluginSharedPtr& plugin); // Stream context. + ~Context() override; + + Wasm* wasm() const; + Plugin* plugin() const; + Context* rootContext() const; + Upstream::ClusterManager& clusterManager() const; + + // proxy_wasm::ContextBase + void error(absl::string_view message) override; + + // Retrieves the stream info associated with the request (a.k.a active stream). + // It selects a value based on the following order: encoder callback, decoder + // callback, log callback, network read filter callback, network write filter + // callback. As long as any one of the callbacks is invoked, the value should be + // available. + const StreamInfo::StreamInfo* getConstRequestStreamInfo() const; + StreamInfo::StreamInfo* getRequestStreamInfo() const; + + // Retrieves the connection object associated with the request (a.k.a active stream). + // It selects a value based on the following order: encoder callback, decoder + // callback. As long as any one of the callbacks is invoked, the value should be + // available. + const Network::Connection* getConnection() const; + + // + // VM level down-calls into the Wasm code on Context(id == 0). + // + virtual bool validateConfiguration(absl::string_view configuration, + const std::shared_ptr& plugin); // deprecated + + // AccessLog::Instance + void log(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap* response_headers, + const Http::ResponseTrailerMap* response_trailers, + const StreamInfo::StreamInfo& stream_info) override; + + uint32_t getLogLevel() override; + + // Network::ConnectionCallbacks + void onEvent(Network::ConnectionEvent event) override; + void onAboveWriteBufferHighWatermark() override {} + void onBelowWriteBufferLowWatermark() override {} + + // Network::ReadFilter + Network::FilterStatus onNewConnection() override; + Network::FilterStatus onData(::Envoy::Buffer::Instance& data, bool end_stream) override; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override; + + // Network::WriteFilter + Network::FilterStatus onWrite(::Envoy::Buffer::Instance& data, bool end_stream) override; + void initializeWriteFilterCallbacks(Network::WriteFilterCallbacks& callbacks) override; + + // proxy_wasm::ContextBase + void onDownstreamConnectionClose(CloseType) override; + void onUpstreamConnectionClose(CloseType) override; + + // Http::StreamFilterBase. Note: This calls onDone() in Wasm. + void onDestroy() override; + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus decodeData(::Envoy::Buffer::Instance& data, bool end_stream) override; + Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap& trailers) override; + Http::FilterMetadataStatus decodeMetadata(Http::MetadataMap& metadata_map) override; + void setDecoderFilterCallbacks(Envoy::Http::StreamDecoderFilterCallbacks& callbacks) override; + + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encode100ContinueHeaders(Http::ResponseHeaderMap&) override; + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus encodeData(::Envoy::Buffer::Instance& data, bool end_stream) override; + Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap& trailers) override; + Http::FilterMetadataStatus encodeMetadata(Http::MetadataMap& metadata_map) override; + void setEncoderFilterCallbacks(Envoy::Http::StreamEncoderFilterCallbacks& callbacks) override; + + // VM calls out to host. + // proxy_wasm::ContextBase + + // General + WasmResult log(uint32_t level, absl::string_view message) override; + uint64_t getCurrentTimeNanoseconds() override; + absl::string_view getConfiguration() override; + std::pair getStatus() override; + + // State accessors + WasmResult getProperty(absl::string_view path, std::string* result) override; + WasmResult setProperty(absl::string_view path, absl::string_view value) override; + WasmResult declareProperty(absl::string_view path, + std::unique_ptr state_prototype); + + // Continue + WasmResult continueStream(WasmStreamType stream_type) override; + WasmResult closeStream(WasmStreamType stream_type) override; + WasmResult sendLocalResponse(uint32_t response_code, absl::string_view body_text, + Pairs additional_headers, uint32_t grpc_status, + absl::string_view details) override; + void clearRouteCache() override { + if (decoder_callbacks_) { + decoder_callbacks_->clearRouteCache(); + } + } + + // Header/Trailer/Metadata Maps + WasmResult addHeaderMapValue(WasmHeaderMapType type, absl::string_view key, + absl::string_view value) override; + WasmResult getHeaderMapValue(WasmHeaderMapType type, absl::string_view key, + absl::string_view* value) override; + WasmResult getHeaderMapPairs(WasmHeaderMapType type, Pairs* result) override; + WasmResult setHeaderMapPairs(WasmHeaderMapType type, const Pairs& pairs) override; + + WasmResult removeHeaderMapValue(WasmHeaderMapType type, absl::string_view key) override; + WasmResult replaceHeaderMapValue(WasmHeaderMapType type, absl::string_view key, + absl::string_view value) override; + + WasmResult getHeaderMapSize(WasmHeaderMapType type, uint32_t* size) override; + + // Buffer + BufferInterface* getBuffer(WasmBufferType type) override; + // TODO: use stream_type. + bool endOfStream(WasmStreamType /* stream_type */) override { return end_of_stream_; } + + // HTTP + WasmResult httpCall(absl::string_view cluster, const Pairs& request_headers, + absl::string_view request_body, const Pairs& request_trailers, + int timeout_millisconds, uint32_t* token_ptr) override; + + // Stats/Metrics + WasmResult defineMetric(uint32_t type, absl::string_view name, uint32_t* metric_id_ptr) override; + WasmResult incrementMetric(uint32_t metric_id, int64_t offset) override; + WasmResult recordMetric(uint32_t metric_id, uint64_t value) override; + WasmResult getMetric(uint32_t metric_id, uint64_t* value_ptr) override; + + // gRPC + WasmResult grpcCall(absl::string_view grpc_service, absl::string_view service_name, + absl::string_view method_name, const Pairs& initial_metadata, + absl::string_view request, std::chrono::milliseconds timeout, + uint32_t* token_ptr) override; + WasmResult grpcStream(absl::string_view grpc_service, absl::string_view service_name, + absl::string_view method_name, const Pairs& initial_metadat, + uint32_t* token_ptr) override; + + WasmResult grpcClose(uint32_t token) override; + WasmResult grpcCancel(uint32_t token) override; + WasmResult grpcSend(uint32_t token, absl::string_view message, bool end_stream) override; + + // Envoy specific ABI + void onResolveDns(uint32_t token, Envoy::Network::DnsResolver::ResolutionStatus status, + std::list&& response); + + void onStatsUpdate(Envoy::Stats::MetricSnapshot& snapshot); + + // CEL evaluation + std::vector + FindFunctionOverloads(absl::string_view) const override { + return {}; + } + absl::optional + findValue(absl::string_view name, Protobuf::Arena* arena, bool last) const; + absl::optional + FindValue(absl::string_view name, Protobuf::Arena* arena) const override { + return findValue(name, arena, false); + } + bool IsPathUnknown(absl::string_view) const override { return false; } + const std::vector& + unknown_attribute_patterns() const override { + static const std::vector empty; + return empty; + } + const Protobuf::FieldMask& unknown_paths() const override { + return Protobuf::FieldMask::default_instance(); + } + + // Foreign function state + virtual void setForeignData(absl::string_view data_name, std::unique_ptr data) { + data_storage_[data_name] = std::move(data); + } + template T* getForeignData(absl::string_view data_name) { + const auto& it = data_storage_.find(data_name); + if (it == data_storage_.end()) { + return nullptr; + } + return dynamic_cast(it->second.get()); + } + + uint32_t nextGrpcCallToken(); + uint32_t nextGrpcStreamToken(); + uint32_t nextHttpCallToken(); + void setNextGrpcTokenForTesting(uint32_t token) { next_grpc_token_ = token; } + void setNextHttpCallTokenForTesting(uint32_t token) { next_http_call_token_ = token; } + +protected: + friend class Wasm; + + void addAfterVmCallAction(std::function f); + void onCloseTCP(); + + struct AsyncClientHandler : public Http::AsyncClient::Callbacks { + // Http::AsyncClient::Callbacks + void onSuccess(const Http::AsyncClient::Request&, + Envoy::Http::ResponseMessagePtr&& response) override { + context_->onHttpCallSuccess(token_, std::move(response)); + } + void onFailure(const Http::AsyncClient::Request&, + Http::AsyncClient::FailureReason reason) override { + context_->onHttpCallFailure(token_, reason); + } + void + onBeforeFinalizeUpstreamSpan(Envoy::Tracing::Span& /* span */, + const Http::ResponseHeaderMap* /* response_headers */) override {} + + Context* context_; + uint32_t token_; + Http::AsyncClient::Request* request_; + }; + + struct GrpcCallClientHandler : public Grpc::RawAsyncRequestCallbacks { + // Grpc::AsyncRequestCallbacks + void onCreateInitialMetadata(Http::RequestHeaderMap& initial_metadata) override { + context_->onGrpcCreateInitialMetadata(token_, initial_metadata); + } + void onSuccessRaw(::Envoy::Buffer::InstancePtr&& response, Tracing::Span& /* span */) override { + context_->onGrpcReceiveWrapper(token_, std::move(response)); + } + void onFailure(Grpc::Status::GrpcStatus status, const std::string& message, + Tracing::Span& /* span */) override { + context_->onGrpcCloseWrapper(token_, status, message); + } + + Context* context_; + uint32_t token_; + Grpc::RawAsyncClientPtr client_; + Grpc::AsyncRequest* request_; + }; + + struct GrpcStreamClientHandler : public Grpc::RawAsyncStreamCallbacks { + // Grpc::AsyncStreamCallbacks + void onCreateInitialMetadata(Http::RequestHeaderMap&) override {} + void onReceiveInitialMetadata(Http::ResponseHeaderMapPtr&& metadata) override { + context_->onGrpcReceiveInitialMetadataWrapper(token_, std::move(metadata)); + } + bool onReceiveMessageRaw(::Envoy::Buffer::InstancePtr&& response) override { + context_->onGrpcReceiveWrapper(token_, std::move(response)); + return true; + } + void onReceiveTrailingMetadata(Http::ResponseTrailerMapPtr&& metadata) override { + context_->onGrpcReceiveTrailingMetadataWrapper(token_, std::move(metadata)); + } + void onRemoteClose(Grpc::Status::GrpcStatus status, const std::string& message) override { + remote_closed_ = true; + context_->onGrpcCloseWrapper(token_, status, message); + } + + Context* context_; + uint32_t token_; + Grpc::RawAsyncClientPtr client_; + Grpc::RawAsyncStream* stream_; + bool local_closed_ = false; + bool remote_closed_ = false; + }; + + void onHttpCallSuccess(uint32_t token, Envoy::Http::ResponseMessagePtr&& response); + void onHttpCallFailure(uint32_t token, Http::AsyncClient::FailureReason reason); + + void onGrpcCreateInitialMetadata(uint32_t token, Http::RequestHeaderMap& metadata); + void onGrpcReceiveInitialMetadataWrapper(uint32_t token, Http::HeaderMapPtr&& metadata); + void onGrpcReceiveWrapper(uint32_t token, ::Envoy::Buffer::InstancePtr response); + void onGrpcReceiveTrailingMetadataWrapper(uint32_t token, Http::HeaderMapPtr&& metadata); + void onGrpcCloseWrapper(uint32_t token, const Grpc::Status::GrpcStatus& status, + const absl::string_view message); + + bool isGrpcStreamToken(uint32_t token) { return (token & 1) == 0; } + bool isGrpcCallToken(uint32_t token) { return (token & 1) == 1; } + + Http::HeaderMap* getMap(WasmHeaderMapType type); + const Http::HeaderMap* getConstMap(WasmHeaderMapType type); + + const LocalInfo::LocalInfo* root_local_info_{nullptr}; // set only for root_context. + + uint32_t next_http_call_token_ = 1; + uint32_t next_grpc_token_ = 1; // Odd tokens are for Calls even for Streams. + + // Network callbacks. + Network::ReadFilterCallbacks* network_read_filter_callbacks_{}; + Network::WriteFilterCallbacks* network_write_filter_callbacks_{}; + + // HTTP callbacks. + Envoy::Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; + Envoy::Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; + + // Status. + uint32_t status_code_{0}; + absl::string_view status_message_; + + // Network filter state. + ::Envoy::Buffer::Instance* network_downstream_data_buffer_{}; + ::Envoy::Buffer::Instance* network_upstream_data_buffer_{}; + + // HTTP filter state. + bool http_request_started_ = false; // When decodeHeaders() is called the request is "started". + Http::RequestHeaderMap* request_headers_{}; + Http::ResponseHeaderMap* response_headers_{}; + ::Envoy::Buffer::Instance* request_body_buffer_{}; + ::Envoy::Buffer::Instance* response_body_buffer_{}; + Http::RequestTrailerMap* request_trailers_{}; + Http::ResponseTrailerMap* response_trailers_{}; + Http::MetadataMap* request_metadata_{}; + Http::MetadataMap* response_metadata_{}; + + // Only available during onHttpCallResponse. + Envoy::Http::ResponseMessagePtr* http_call_response_{}; + + Http::HeaderMapPtr grpc_receive_initial_metadata_{}; + Http::HeaderMapPtr grpc_receive_trailing_metadata_{}; + + // Only available (non-nullptr) during onGrpcReceive. + ::Envoy::Buffer::InstancePtr grpc_receive_buffer_; + + // Only available (non-nullptr) during grpcCall and grpcStream. + Http::RequestHeaderMapPtr grpc_initial_metadata_; + + // Access log state. + const StreamInfo::StreamInfo* access_log_stream_info_{}; + const Http::RequestHeaderMap* access_log_request_headers_{}; + const Http::ResponseHeaderMap* access_log_response_headers_{}; + const Http::ResponseTrailerMap* access_log_response_trailers_{}; + + // Temporary state. + ProtobufWkt::Struct temporary_metadata_; + bool end_of_stream_; + bool buffering_request_body_ = false; + bool buffering_response_body_ = false; + Buffer buffer_; + + // MB: must be a node-type map as we take persistent references to the entries. + std::map http_request_; + std::map grpc_call_request_; + std::map grpc_stream_; + + // Opaque state. + absl::flat_hash_map> data_storage_; + + // TCP State. + bool upstream_closed_ = false; + bool downstream_closed_ = false; + bool tcp_connection_closed_ = false; + + // Filter state prototype declaration. + absl::flat_hash_map> state_prototypes_; +}; +using ContextSharedPtr = std::shared_ptr; + +WasmResult serializeValue(Filters::Common::Expr::CelValue value, std::string* result); + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/ext/BUILD b/source/extensions/common/wasm/ext/BUILD new file mode 100644 index 000000000000..286c0774edfe --- /dev/null +++ b/source/extensions/common/wasm/ext/BUILD @@ -0,0 +1,95 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_proto_library") +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "envoy_null_vm_wasm_api", + hdrs = [ + "envoy_null_vm_wasm_api.h", + "envoy_proxy_wasm_api.h", + ], + visibility = ["//visibility:public"], + deps = [ + "@proxy_wasm_cpp_sdk//:api_lib", + "@proxy_wasm_cpp_sdk//:common_lib", + ], +) + +envoy_cc_library( + name = "envoy_null_plugin", + hdrs = [ + "envoy_null_plugin.h", + "envoy_proxy_wasm_api.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":declare_property_cc_proto", + "//source/common/grpc:async_client_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +# NB: this target is compiled to Wasm. Hence the generic rule. +cc_library( + name = "envoy_proxy_wasm_api_lib", + srcs = ["envoy_proxy_wasm_api.cc"], + hdrs = ["envoy_proxy_wasm_api.h"], + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [ + ":declare_property_cc_proto", + ":node_subset_cc_proto", + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], + alwayslink = 1, +) + +# NB: this target is compiled both to native code and to Wasm. Hence the generic rule. +proto_library( + name = "declare_property_proto", + srcs = ["declare_property.proto"], + visibility = ["//visibility:public"], +) + +# NB: this target is compiled both to native code and to Wasm. Hence the generic rule. +cc_proto_library( + name = "declare_property_cc_proto", + visibility = ["//visibility:public"], + deps = [":declare_property_proto"], +) + +# NB: this target is compiled both to native code and to Wasm. Hence the generic rule. +proto_library( + name = "node_subset_proto", + srcs = ["node_subset.proto"], + visibility = ["//visibility:public"], + deps = [ + "@com_google_protobuf//:struct_proto", + ], +) + +# NB: this target is compiled both to native code and to Wasm. Hence the generic rule. +cc_proto_library( + name = "node_subset_cc_proto", + visibility = ["//visibility:public"], + deps = [ + ":node_subset_proto", + # "//external:protobuf_clib", + ], +) + +filegroup( + name = "jslib", + srcs = [ + "envoy_wasm_intrinsics.js", + ], + visibility = ["//visibility:public"], +) diff --git a/source/extensions/common/wasm/ext/README.md b/source/extensions/common/wasm/ext/README.md new file mode 100644 index 000000000000..b9e1e44d4dbc --- /dev/null +++ b/source/extensions/common/wasm/ext/README.md @@ -0,0 +1 @@ +# Envoy specific extensions to the proxy-wasm SDK diff --git a/source/extensions/common/wasm/ext/declare_property.proto b/source/extensions/common/wasm/ext/declare_property.proto new file mode 100644 index 000000000000..b08ce6375481 --- /dev/null +++ b/source/extensions/common/wasm/ext/declare_property.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package envoy.source.extensions.common.wasm; + +enum WasmType { + Bytes = 0; + String = 1; + FlatBuffers = 2; + Protobuf = 3; +}; + +enum LifeSpan { + FilterChain = 0; + DownstreamRequest = 1; + DownstreamConnection = 2; +}; + +message DeclarePropertyArguments { + string name = 1; + bool readonly = 2; + WasmType type = 3; + bytes schema = 4; + LifeSpan span = 5; +}; diff --git a/source/extensions/common/wasm/ext/envoy_null_plugin.h b/source/extensions/common/wasm/ext/envoy_null_plugin.h new file mode 100644 index 000000000000..1463e00e1ef5 --- /dev/null +++ b/source/extensions/common/wasm/ext/envoy_null_plugin.h @@ -0,0 +1,48 @@ +// NOLINT(namespace-envoy) +#pragma once + +#define PROXY_WASM_PROTOBUF 1 +#define PROXY_WASM_PROTOBUF_FULL 1 + +#include "envoy/config/core/v3/grpc_service.pb.h" + +#include "source/extensions/common/wasm/ext/declare_property.pb.h" + +#include "include/proxy-wasm/null_plugin.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +proxy_wasm::Word resolve_dns(void* raw_context, proxy_wasm::Word dns_address, + proxy_wasm::Word dns_address_size, proxy_wasm::Word token_ptr); + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy + +namespace proxy_wasm { +namespace null_plugin { + +#include "extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +using GrpcService = envoy::config::core::v3::GrpcService; +using namespace proxy_wasm::null_plugin; + +#define WS(_x) Word(static_cast(_x)) +#define WR(_x) Word(reinterpret_cast(_x)) + +inline WasmResult envoy_resolve_dns(const char* dns_address, size_t dns_address_size, + uint32_t* token) { + return static_cast( + ::Envoy::Extensions::Common::Wasm::resolve_dns(proxy_wasm::current_context_, WR(dns_address), + WS(dns_address_size), WR(token)) + .u64_); +} + +#undef WS +#undef WR + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/source/extensions/common/wasm/ext/envoy_null_vm_wasm_api.h b/source/extensions/common/wasm/ext/envoy_null_vm_wasm_api.h new file mode 100644 index 000000000000..f6415b3fd2fa --- /dev/null +++ b/source/extensions/common/wasm/ext/envoy_null_vm_wasm_api.h @@ -0,0 +1,24 @@ +// NOLINT(namespace-envoy) +#pragma once + +namespace proxy_wasm { +namespace null_plugin { + +#include "proxy_wasm_common.h" +#include "proxy_wasm_enums.h" +#include "proxy_wasm_externs.h" + +/* + * The following headers are used in two different environments, in the Null VM and in Wasm code + * which require different headers to precede these such that they can not include the above + * headers directly. These macros prevent header reordering + */ +#define _THE_FOLLOWING_INCLUDE_MUST_COME_AFTER_THOSE_ABOVE_ 1 +#include "proxy_wasm_api.h" +#undef _THE_FOLLOWING_INCLUDE_MUST_COME_AFTER_THOSE_ABOVE_ +#define _THE_FOLLOWING_INCLUDE_MUST_COME_AFTER_THOSE_ABOVE_ 1 +#include "extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +#undef _THE_FOLLOWING_INCLUDE_MUST_COME_AFTER_THOSE_ABOVE_ + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/source/extensions/common/wasm/ext/envoy_proxy_wasm_api.cc b/source/extensions/common/wasm/ext/envoy_proxy_wasm_api.cc new file mode 100644 index 000000000000..cb0cb3429144 --- /dev/null +++ b/source/extensions/common/wasm/ext/envoy_proxy_wasm_api.cc @@ -0,0 +1,40 @@ +// NOLINT(namespace-envoy) + +#include "proxy_wasm_intrinsics.h" + +/* + * These headers span repositories and therefor the following header can not include the above + * header to enforce the required order. This macros prevent header reordering. + */ +#define _THE_FOLLOWING_INCLUDE_MUST_COME_AFTER_THOSE_ABOVE_ 1 +#include "source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +#undef _THE_FOLLOWING_INCLUDE_MUST_COME_AFTER_THOSE_ABOVE_ + +EnvoyContextBase* getEnvoyContextBase(uint32_t context_id) { + auto context_base = getContextBase(context_id); + if (auto root = context_base->asRoot()) { + return static_cast(static_cast(root)); + } else { + return static_cast(static_cast(context_base->asContext())); + } +} + +EnvoyContext* getEnvoyContext(uint32_t context_id) { + auto context_base = getContextBase(context_id); + return static_cast(context_base->asContext()); +} + +EnvoyRootContext* getEnvoyRootContext(uint32_t context_id) { + auto context_base = getContextBase(context_id); + return static_cast(context_base->asRoot()); +} + +extern "C" PROXY_WASM_KEEPALIVE void envoy_on_resolve_dns(uint32_t context_id, uint32_t token, + uint32_t data_size) { + getEnvoyRootContext(context_id)->onResolveDns(token, data_size); +} + +extern "C" PROXY_WASM_KEEPALIVE void envoy_on_stats_update(uint32_t context_id, + uint32_t data_size) { + getEnvoyRootContext(context_id)->onStatsUpdate(data_size); +} diff --git a/source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h b/source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h new file mode 100644 index 000000000000..601713012c5a --- /dev/null +++ b/source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h @@ -0,0 +1,131 @@ +// NOLINT(namespace-envoy) +#pragma once + +// Note that this file is included in emscripten and NullVM environments and thus depends on +// the context in which it is included, hence we need to disable clang-tidy warnings. + +extern "C" WasmResult envoy_resolve_dns(const char* dns_address, size_t dns_address_size, + uint32_t* token); + +class EnvoyContextBase { +public: + virtual ~EnvoyContextBase() = default; +}; + +class EnvoyRootContext : public RootContext, public EnvoyContextBase { +public: + EnvoyRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {} + ~EnvoyRootContext() override = default; + + virtual void onResolveDns(uint32_t /* token */, uint32_t /* result_size */) {} + virtual void onStatsUpdate(uint32_t /* result_size */) {} +}; + +class EnvoyContext : public Context, public EnvoyContextBase { +public: + EnvoyContext(uint32_t id, RootContext* root) : Context(id, root) {} + ~EnvoyContext() override = default; +}; + +struct DnsResult { + uint32_t ttl_seconds; + std::string address; +}; + +struct CounterResult { + uint64_t delta; + std::string_view name; + uint64_t value; +}; + +struct GaugeResult { + uint64_t value; + std::string_view name; +}; + +struct StatResult { + std::vector counters; + std::vector gauges; +}; + +enum class StatType : uint32_t { + Counter = 1, + Gauge = 2, +}; + +inline std::vector parseDnsResults(std::string_view data) { + if (data.size() < 4) { + return {}; + } + const uint32_t* pn = reinterpret_cast(data.data()); + uint32_t n = *pn++; + std::vector results; + results.resize(n); + const char* pa = data.data() + (1 + n) * sizeof(uint32_t); // skip n + n TTLs + for (uint32_t i = 0; i < n; i++) { + auto& e = results[i]; + e.ttl_seconds = *pn++; + auto alen = strlen(pa); + e.address.assign(pa, alen); + pa += alen + 1; + } + return results; +} + +template inline uint32_t align(uint32_t i) { + return (i + sizeof(I) - 1) & ~(sizeof(I) - 1); +} + +inline StatResult parseStatResults(std::string_view data) { + StatResult results; + uint32_t data_len = 0; + while (data_len < data.length()) { + const uint32_t* n = reinterpret_cast(data.data() + data_len); + uint32_t block_size = *n++; + uint32_t block_type = *n++; + uint32_t num_stats = *n++; + if (static_cast(block_type) == StatType::Counter) { // counter + std::vector counters(num_stats); + uint32_t stat_index = data_len + 3 * sizeof(uint32_t); + for (uint32_t i = 0; i < num_stats; i++) { + const uint32_t* stat_name = reinterpret_cast(data.data() + stat_index); + uint32_t name_len = *stat_name; + stat_index += sizeof(uint32_t); + + auto& e = counters[i]; + e.name = {data.data() + stat_index, name_len}; + stat_index = align(stat_index + name_len); + + const uint64_t* stat_vals = reinterpret_cast(data.data() + stat_index); + e.value = *stat_vals++; + e.delta = *stat_vals++; + + stat_index += 2 * sizeof(uint64_t); + } + results.counters = counters; + } else if (static_cast(block_type) == StatType::Gauge) { // gauge + std::vector gauges(num_stats); + uint32_t stat_index = data_len + 3 * sizeof(uint32_t); + for (uint32_t i = 0; i < num_stats; i++) { + const uint32_t* stat_name = reinterpret_cast(data.data() + stat_index); + uint32_t name_len = *stat_name; + stat_index += sizeof(uint32_t); + + auto& e = gauges[i]; + e.name = {data.data() + stat_index, name_len}; + stat_index = align(stat_index + name_len); + + const uint64_t* stat_vals = reinterpret_cast(data.data() + stat_index); + e.value = *stat_vals++; + + stat_index += sizeof(uint64_t); + } + results.gauges = gauges; + } + data_len += block_size; + } + + return results; +} + +extern "C" WasmResult envoy_resolve_dns(const char* address, size_t address_size, uint32_t* token); diff --git a/source/extensions/common/wasm/ext/envoy_wasm_intrinsics.js b/source/extensions/common/wasm/ext/envoy_wasm_intrinsics.js new file mode 100644 index 000000000000..116a5b8a3867 --- /dev/null +++ b/source/extensions/common/wasm/ext/envoy_wasm_intrinsics.js @@ -0,0 +1,3 @@ +mergeInto(LibraryManager.library, { + envoy_resolve_dns: function() {}, +}); diff --git a/source/extensions/common/wasm/ext/node_subset.proto b/source/extensions/common/wasm/ext/node_subset.proto new file mode 100644 index 000000000000..9e766c4b12b0 --- /dev/null +++ b/source/extensions/common/wasm/ext/node_subset.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +import "google/protobuf/struct.proto"; + +package envoy.source.extensions.common.wasm; + +// A subset of message Node from api/envoy/config/core/v?/base.proto. +message NodeSubset { + string id = 1; + google.protobuf.Struct metadata = 3; +}; diff --git a/source/extensions/common/wasm/foreign.cc b/source/extensions/common/wasm/foreign.cc new file mode 100644 index 000000000000..565ca22adb9b --- /dev/null +++ b/source/extensions/common/wasm/foreign.cc @@ -0,0 +1,277 @@ +#include "common/common/logger.h" + +#include "source/extensions/common/wasm/ext/declare_property.pb.h" + +#include "extensions/common/wasm/wasm.h" + +#if defined(WASM_USE_CEL_PARSER) +#include "eval/public/builtin_func_registrar.h" +#include "eval/public/cel_expr_builder_factory.h" +#include "parser/parser.h" +#endif +#include "zlib.h" + +using proxy_wasm::RegisterForeignFunction; +using proxy_wasm::WasmForeignFunction; + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +template WasmForeignFunction createFromClass() { + auto c = std::make_shared(); + return c->create(c); +} + +RegisterForeignFunction registerCompressForeignFunction( + "compress", + [](WasmBase&, absl::string_view arguments, + const std::function& alloc_result) -> WasmResult { + unsigned long dest_len = compressBound(arguments.size()); + std::unique_ptr b(new unsigned char[dest_len]); + if (compress(b.get(), &dest_len, reinterpret_cast(arguments.data()), + arguments.size()) != Z_OK) { + return WasmResult::SerializationFailure; + } + auto result = alloc_result(dest_len); + memcpy(result, b.get(), dest_len); + return WasmResult::Ok; + }); + +RegisterForeignFunction registerUncompressForeignFunction( + "uncompress", + [](WasmBase&, absl::string_view arguments, + const std::function& alloc_result) -> WasmResult { + unsigned long dest_len = arguments.size() * 2 + 2; // output estimate. + while (true) { + std::unique_ptr b(new unsigned char[dest_len]); + auto r = + uncompress(b.get(), &dest_len, reinterpret_cast(arguments.data()), + arguments.size()); + if (r == Z_OK) { + auto result = alloc_result(dest_len); + memcpy(result, b.get(), dest_len); + return WasmResult::Ok; + } + if (r != Z_BUF_ERROR) { + return WasmResult::SerializationFailure; + } + dest_len = dest_len * 2; + } + }); + +#if defined(WASM_USE_CEL_PARSER) +class ExpressionFactory : public Logger::Loggable { +protected: + struct ExpressionData { + google::api::expr::v1alpha1::ParsedExpr parsed_expr_; + Filters::Common::Expr::ExpressionPtr compiled_expr_; + }; + + class ExpressionContext : public StorageObject { + public: + friend class ExpressionFactory; + ExpressionContext(Filters::Common::Expr::BuilderPtr builder) : builder_(std::move(builder)) {} + uint32_t createToken() { + uint32_t token = next_expr_token_++; + for (;;) { + if (!expr_.count(token)) { + break; + } + token = next_expr_token_++; + } + return token; + } + bool hasExpression(uint32_t token) { return expr_.contains(token); } + ExpressionData& getExpression(uint32_t token) { return expr_[token]; } + void deleteExpression(uint32_t token) { expr_.erase(token); } + Filters::Common::Expr::Builder* builder() { return builder_.get(); } + + private: + Filters::Common::Expr::BuilderPtr builder_{}; + uint32_t next_expr_token_ = 0; + absl::flat_hash_map expr_; + }; + + static ExpressionContext& getOrCreateContext(ContextBase* context_base) { + auto context = static_cast(context_base); + std::string data_name = "cel"; + auto expr_context = context->getForeignData(data_name); + if (!expr_context) { + google::api::expr::runtime::InterpreterOptions options; + auto builder = google::api::expr::runtime::CreateCelExpressionBuilder(options); + auto status = + google::api::expr::runtime::RegisterBuiltinFunctions(builder->GetRegistry(), options); + if (!status.ok()) { + ENVOY_LOG(warn, "failed to register built-in functions: {}", status.message()); + } + auto new_context = std::make_unique(std::move(builder)); + expr_context = new_context.get(); + context->setForeignData(data_name, std::move(new_context)); + } + return *expr_context; + } +}; + +class CreateExpressionFactory : public ExpressionFactory { +public: + WasmForeignFunction create(std::shared_ptr self) const { + WasmForeignFunction f = + [self](WasmBase&, absl::string_view expr, + const std::function& alloc_result) -> WasmResult { + auto parse_status = google::api::expr::parser::Parse(std::string(expr)); + if (!parse_status.ok()) { + ENVOY_LOG(info, "expr_create parse error: {}", parse_status.status().message()); + return WasmResult::BadArgument; + } + + auto& expr_context = getOrCreateContext(proxy_wasm::current_context_->root_context()); + auto token = expr_context.createToken(); + auto& handler = expr_context.getExpression(token); + + handler.parsed_expr_ = parse_status.value(); + auto cel_expression_status = expr_context.builder()->CreateExpression( + &handler.parsed_expr_.expr(), &handler.parsed_expr_.source_info()); + if (!cel_expression_status.ok()) { + ENVOY_LOG(info, "expr_create compile error: {}", cel_expression_status.status().message()); + expr_context.deleteExpression(token); + return WasmResult::BadArgument; + } + + handler.compiled_expr_ = std::move(cel_expression_status.value()); + auto result = reinterpret_cast(alloc_result(sizeof(uint32_t))); + *result = token; + return WasmResult::Ok; + }; + return f; + } +}; +RegisterForeignFunction + registerCreateExpressionForeignFunction("expr_create", + createFromClass()); + +class EvaluateExpressionFactory : public ExpressionFactory { +public: + WasmForeignFunction create(std::shared_ptr self) const { + WasmForeignFunction f = + [self](WasmBase&, absl::string_view argument, + const std::function& alloc_result) -> WasmResult { + auto& expr_context = getOrCreateContext(proxy_wasm::current_context_->root_context()); + if (argument.size() != sizeof(uint32_t)) { + return WasmResult::BadArgument; + } + uint32_t token = *reinterpret_cast(argument.data()); + if (!expr_context.hasExpression(token)) { + return WasmResult::NotFound; + } + Protobuf::Arena arena; + auto& handler = expr_context.getExpression(token); + auto context = static_cast(proxy_wasm::current_context_); + auto eval_status = handler.compiled_expr_->Evaluate(*context, &arena); + if (!eval_status.ok()) { + ENVOY_LOG(debug, "expr_evaluate error: {}", eval_status.status().message()); + return WasmResult::InternalFailure; + } + auto value = eval_status.value(); + if (value.IsError()) { + ENVOY_LOG(debug, "expr_evaluate value error: {}", value.ErrorOrDie()->message()); + return WasmResult::InternalFailure; + } + std::string result; + auto serialize_status = serializeValue(value, &result); + if (serialize_status != WasmResult::Ok) { + return serialize_status; + } + auto output = alloc_result(result.size()); + memcpy(output, result.data(), result.size()); + return WasmResult::Ok; + }; + return f; + } +}; +RegisterForeignFunction + registerEvaluateExpressionForeignFunction("expr_evaluate", + createFromClass()); + +class DeleteExpressionFactory : public ExpressionFactory { +public: + WasmForeignFunction create(std::shared_ptr self) const { + WasmForeignFunction f = [self](WasmBase&, absl::string_view argument, + const std::function&) -> WasmResult { + auto& expr_context = getOrCreateContext(proxy_wasm::current_context_->root_context()); + if (argument.size() != sizeof(uint32_t)) { + return WasmResult::BadArgument; + } + uint32_t token = *reinterpret_cast(argument.data()); + expr_context.deleteExpression(token); + return WasmResult::Ok; + }; + return f; + } +}; +RegisterForeignFunction + registerDeleteExpressionForeignFunction("expr_delete", + createFromClass()); +#endif + +// TODO(kyessenov) The factories should be separated into individual compilation units. +// TODO(kyessenov) Leverage the host argument marshaller instead of the protobuf argument list. +class DeclarePropertyFactory { +public: + WasmForeignFunction create(std::shared_ptr self) const { + WasmForeignFunction f = [self](WasmBase&, absl::string_view arguments, + const std::function&) -> WasmResult { + envoy::source::extensions::common::wasm::DeclarePropertyArguments args; + if (args.ParseFromArray(arguments.data(), arguments.size())) { + WasmType type = WasmType::Bytes; + switch (args.type()) { + case envoy::source::extensions::common::wasm::WasmType::Bytes: + type = WasmType::Bytes; + break; + case envoy::source::extensions::common::wasm::WasmType::Protobuf: + type = WasmType::Protobuf; + break; + case envoy::source::extensions::common::wasm::WasmType::String: + type = WasmType::String; + break; + case envoy::source::extensions::common::wasm::WasmType::FlatBuffers: + type = WasmType::FlatBuffers; + break; + default: + // do nothing + break; + } + StreamInfo::FilterState::LifeSpan span = StreamInfo::FilterState::LifeSpan::FilterChain; + switch (args.span()) { + case envoy::source::extensions::common::wasm::LifeSpan::FilterChain: + span = StreamInfo::FilterState::LifeSpan::FilterChain; + break; + case envoy::source::extensions::common::wasm::LifeSpan::DownstreamRequest: + span = StreamInfo::FilterState::LifeSpan::Request; + break; + case envoy::source::extensions::common::wasm::LifeSpan::DownstreamConnection: + span = StreamInfo::FilterState::LifeSpan::Connection; + break; + default: + // do nothing + break; + } + auto context = static_cast(proxy_wasm::current_context_); + return context->declareProperty( + args.name(), + std::make_unique(args.readonly(), type, args.schema(), span)); + } + return WasmResult::BadArgument; + }; + return f; + } +}; +RegisterForeignFunction + registerDeclarePropertyForeignFunction("declare_property", + createFromClass()); + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/null/BUILD b/source/extensions/common/wasm/null/BUILD deleted file mode 100644 index 31a33d8f4d49..000000000000 --- a/source/extensions/common/wasm/null/BUILD +++ /dev/null @@ -1,49 +0,0 @@ -load( - "//bazel:envoy_build_system.bzl", - "envoy_cc_library", - "envoy_extension_package", -) - -licenses(["notice"]) # Apache 2 - -envoy_extension_package() - -envoy_cc_library( - name = "null_vm_plugin_interface", - hdrs = ["null_vm_plugin.h"], - deps = [ - "//include/envoy/config:typed_config_interface", - "//source/extensions/common/wasm:wasm_vm_interface", - "//source/extensions/common/wasm:well_known_names", - ], -) - -envoy_cc_library( - name = "null_vm_lib", - srcs = ["null_vm.cc"], - hdrs = ["null_vm.h"], - deps = [ - ":null_vm_plugin_interface", - "//external:abseil_node_hash_map", - "//include/envoy/registry", - "//source/common/common:assert_lib", - "//source/extensions/common/wasm:wasm_vm_base", - "//source/extensions/common/wasm:wasm_vm_interface", - "//source/extensions/common/wasm:well_known_names", - ], -) - -envoy_cc_library( - name = "null_lib", - srcs = ["null.cc"], - hdrs = ["null.h"], - deps = [ - ":null_vm_lib", - ":null_vm_plugin_interface", - "//external:abseil_node_hash_map", - "//include/envoy/registry", - "//source/common/common:assert_lib", - "//source/extensions/common/wasm:wasm_vm_interface", - "//source/extensions/common/wasm:well_known_names", - ], -) diff --git a/source/extensions/common/wasm/null/null.cc b/source/extensions/common/wasm/null/null.cc deleted file mode 100644 index af2ba77d1dc5..000000000000 --- a/source/extensions/common/wasm/null/null.cc +++ /dev/null @@ -1,27 +0,0 @@ -#include "extensions/common/wasm/null/null.h" - -#include -#include -#include - -#include "envoy/registry/registry.h" - -#include "common/common/assert.h" - -#include "extensions/common/wasm/null/null_vm.h" -#include "extensions/common/wasm/null/null_vm_plugin.h" -#include "extensions/common/wasm/well_known_names.h" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace Null { - -WasmVmPtr createVm(const Stats::ScopeSharedPtr& scope) { return std::make_unique(scope); } - -} // namespace Null -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null.h b/source/extensions/common/wasm/null/null.h deleted file mode 100644 index 285b13373fbc..000000000000 --- a/source/extensions/common/wasm/null/null.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - -#include "extensions/common/wasm/null/null_vm_plugin.h" -#include "extensions/common/wasm/wasm_vm.h" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace Null { - -WasmVmPtr createVm(const Stats::ScopeSharedPtr& scope); - -} // namespace Null -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null_vm.cc b/source/extensions/common/wasm/null/null_vm.cc deleted file mode 100644 index abd4418fc76b..000000000000 --- a/source/extensions/common/wasm/null/null_vm.cc +++ /dev/null @@ -1,94 +0,0 @@ -#include "extensions/common/wasm/null/null_vm.h" - -#include -#include -#include - -#include "envoy/registry/registry.h" - -#include "common/common/assert.h" - -#include "extensions/common/wasm/null/null_vm_plugin.h" -#include "extensions/common/wasm/well_known_names.h" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace Null { - -WasmVmPtr NullVm::clone() { - auto cloned_null_vm = std::make_unique(*this); - cloned_null_vm->load(plugin_name_, false /* unused */); - return cloned_null_vm; -} - -// "Load" the plugin by obtaining a pointer to it from the factory. -bool NullVm::load(const std::string& name, bool /* allow_precompiled */) { - auto factory = Registry::FactoryRegistry::getFactory(name); - if (!factory) { - return false; - } - plugin_name_ = name; - plugin_ = factory->create(); - return true; -} - -void NullVm::link(absl::string_view /* name */) {} - -uint64_t NullVm::getMemorySize() { return std::numeric_limits::max(); } - -// NulVm pointers are just native pointers. -absl::optional NullVm::getMemory(uint64_t pointer, uint64_t size) { - if (pointer == 0 && size != 0) { - return absl::nullopt; - } - return absl::string_view(reinterpret_cast(pointer), static_cast(size)); -} - -bool NullVm::setMemory(uint64_t pointer, uint64_t size, const void* data) { - if ((pointer == 0 || data == nullptr)) { - if (size != 0) { - return false; - } else { - return true; - } - } - auto p = reinterpret_cast(pointer); - memcpy(p, data, size); - return true; -} - -bool NullVm::setWord(uint64_t pointer, Word data) { - if (pointer == 0) { - return false; - } - auto p = reinterpret_cast(pointer); - memcpy(p, &data.u64_, sizeof(data.u64_)); - return true; -} - -bool NullVm::getWord(uint64_t pointer, Word* data) { - if (pointer == 0) { - return false; - } - auto p = reinterpret_cast(pointer); - memcpy(&data->u64_, p, sizeof(data->u64_)); - return true; -} - -absl::string_view NullVm::getCustomSection(absl::string_view /* name */) { - // Return nothing: there is no WASM file. - return {}; -} - -absl::string_view NullVm::getPrecompiledSectionName() { - // Return nothing: there is no WASM file. - return {}; -} - -} // namespace Null -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null_vm.h b/source/extensions/common/wasm/null/null_vm.h deleted file mode 100644 index 9bdaad668f8b..000000000000 --- a/source/extensions/common/wasm/null/null_vm.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "envoy/registry/registry.h" - -#include "common/common/assert.h" - -#include "extensions/common/wasm/null/null_vm_plugin.h" -#include "extensions/common/wasm/wasm_vm_base.h" -#include "extensions/common/wasm/well_known_names.h" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace Null { - -// The NullVm wraps a C++ WASM plugin which has been compiled with the WASM API -// and linked directly into the Envoy process. This is useful for development -// in that it permits the debugger to set breakpoints in both Envoy and the plugin. -struct NullVm : public WasmVmBase { - NullVm(const Stats::ScopeSharedPtr& scope) : WasmVmBase(scope, WasmRuntimeNames::get().Null) {} - NullVm(const NullVm& other) - : WasmVmBase(other.scope_, WasmRuntimeNames::get().Null), plugin_name_(other.plugin_name_) {} - - // WasmVm - absl::string_view runtime() override { return WasmRuntimeNames::get().Null; } - Cloneable cloneable() override { return Cloneable::InstantiatedModule; }; - WasmVmPtr clone() override; - bool load(const std::string& code, bool allow_precompiled) override; - void link(absl::string_view debug_name) override; - uint64_t getMemorySize() override; - absl::optional getMemory(uint64_t pointer, uint64_t size) override; - bool setMemory(uint64_t pointer, uint64_t size, const void* data) override; - bool setWord(uint64_t pointer, Word data) override; - bool getWord(uint64_t pointer, Word* data) override; - absl::string_view getCustomSection(absl::string_view name) override; - absl::string_view getPrecompiledSectionName() override; - -#define _FORWARD_GET_FUNCTION(_T) \ - void getFunction(absl::string_view function_name, _T* f) override { \ - plugin_->getFunction(function_name, f); \ - } - FOR_ALL_WASM_VM_EXPORTS(_FORWARD_GET_FUNCTION) -#undef _FORWARD_GET_FUNCTION - - // These are not needed for NullVm which invokes the handlers directly. -#define _REGISTER_CALLBACK(_T) \ - void registerCallback(absl::string_view, absl::string_view, _T, \ - typename ConvertFunctionTypeWordToUint32<_T>::type) override{}; - FOR_ALL_WASM_VM_IMPORTS(_REGISTER_CALLBACK) -#undef _REGISTER_CALLBACK - - std::string plugin_name_; - NullVmPluginPtr plugin_; -}; - -} // namespace Null -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null_vm_plugin.h b/source/extensions/common/wasm/null/null_vm_plugin.h deleted file mode 100644 index 1176c98c07c9..000000000000 --- a/source/extensions/common/wasm/null/null_vm_plugin.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include - -#include "envoy/config/typed_config.h" - -#include "extensions/common/wasm/wasm_vm.h" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace Null { - -// A wrapper for the natively compiled NullVm plugin which implements the WASM ABI. -class NullVmPlugin { -public: - NullVmPlugin() = default; - virtual ~NullVmPlugin() = default; - - // NB: These are defined rather than declared PURE because gmock uses __LINE__ internally for - // uniqueness, making it impossible to use FOR_ALL_WASM_VM_EXPORTS with MOCK_METHOD. -#define _DEFINE_GET_FUNCTION(_T) \ - virtual void getFunction(absl::string_view, _T* f) { *f = nullptr; } - FOR_ALL_WASM_VM_EXPORTS(_DEFINE_GET_FUNCTION) -#undef _DEFIN_GET_FUNCTIONE -}; - -using NullVmPluginPtr = std::unique_ptr; - -/** - * Pseudo-WASM plugins using the NullVM should implement this factory and register via - * Registry::registerFactory or the convenience class RegisterFactory. - */ -class NullVmPluginFactory : public Config::UntypedFactory { -public: - ~NullVmPluginFactory() override = default; - - std::string category() const override { return "envoy.wasm.null_vms"; } - - /** - * Create an instance of the plugin. - */ - virtual NullVmPluginPtr create() const PURE; -}; - -} // namespace Null -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/v8/BUILD b/source/extensions/common/wasm/v8/BUILD deleted file mode 100644 index 4ff62d112f2f..000000000000 --- a/source/extensions/common/wasm/v8/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -load( - "//bazel:envoy_build_system.bzl", - "envoy_cc_library", - "envoy_extension_package", -) - -licenses(["notice"]) # Apache 2 - -envoy_extension_package() - -envoy_cc_library( - name = "v8_lib", - srcs = ["v8.cc"], - hdrs = ["v8.h"], - external_deps = [ - "wee8", - ], - deps = [ - "//source/common/common:assert_lib", - "//source/extensions/common/wasm:wasm_vm_base", - "//source/extensions/common/wasm:wasm_vm_interface", - "//source/extensions/common/wasm:well_known_names", - ], -) diff --git a/source/extensions/common/wasm/v8/v8.cc b/source/extensions/common/wasm/v8/v8.cc deleted file mode 100644 index b9c3673315d2..000000000000 --- a/source/extensions/common/wasm/v8/v8.cc +++ /dev/null @@ -1,678 +0,0 @@ -#include "extensions/common/wasm/v8/v8.h" - -#include -#include -#include - -#include "common/common/assert.h" - -#include "extensions/common/wasm/wasm_vm_base.h" -#include "extensions/common/wasm/well_known_names.h" - -#include "absl/container/flat_hash_map.h" -#include "absl/strings/match.h" -#include "v8-version.h" -#include "wasm-api/wasm.hh" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace V8 { - -wasm::Engine* engine() { - static const auto engine = wasm::Engine::make(); - return engine.get(); -} - -struct FuncData { - FuncData(std::string name) : name_(std::move(name)) {} - - std::string name_; - wasm::own callback_; - void* raw_func_; -}; - -using FuncDataPtr = std::unique_ptr; - -class V8 : public WasmVmBase { -public: - V8(const Stats::ScopeSharedPtr& scope) : WasmVmBase(scope, WasmRuntimeNames::get().V8) {} - - // Extensions::Common::Wasm::WasmVm - absl::string_view runtime() override { return WasmRuntimeNames::get().V8; } - - bool load(const std::string& code, bool allow_precompiled) override; - absl::string_view getCustomSection(absl::string_view name) override; - absl::string_view getPrecompiledSectionName() override; - void link(absl::string_view debug_name) override; - - Cloneable cloneable() override { return Cloneable::CompiledBytecode; } - WasmVmPtr clone() override; - - uint64_t getMemorySize() override; - absl::optional getMemory(uint64_t pointer, uint64_t size) override; - bool setMemory(uint64_t pointer, uint64_t size, const void* data) override; - bool getWord(uint64_t pointer, Word* word) override; - bool setWord(uint64_t pointer, Word word) override; - -#define _REGISTER_HOST_FUNCTION(T) \ - void registerCallback(absl::string_view module_name, absl::string_view function_name, T, \ - typename ConvertFunctionTypeWordToUint32::type f) override { \ - registerHostFunctionImpl(module_name, function_name, f); \ - }; - FOR_ALL_WASM_VM_IMPORTS(_REGISTER_HOST_FUNCTION) -#undef _REGISTER_HOST_FUNCTION - -#define _GET_MODULE_FUNCTION(T) \ - void getFunction(absl::string_view function_name, T* f) override { \ - getModuleFunctionImpl(function_name, f); \ - }; - FOR_ALL_WASM_VM_EXPORTS(_GET_MODULE_FUNCTION) -#undef _GET_MODULE_FUNCTION - -private: - wasm::vec getStrippedSource(); - - template - void registerHostFunctionImpl(absl::string_view module_name, absl::string_view function_name, - void (*function)(void*, Args...)); - - template - void registerHostFunctionImpl(absl::string_view module_name, absl::string_view function_name, - R (*function)(void*, Args...)); - - template - void getModuleFunctionImpl(absl::string_view function_name, - std::function* function); - - template - void getModuleFunctionImpl(absl::string_view function_name, - std::function* function); - - wasm::vec source_ = wasm::vec::invalid(); - wasm::own store_; - wasm::own module_; - wasm::own> shared_module_; - wasm::own instance_; - wasm::own memory_; - wasm::own table_; - - absl::flat_hash_map host_functions_; - absl::flat_hash_map> module_functions_; -}; - -// Helper functions. - -static std::string printValue(const wasm::Val& value) { - switch (value.kind()) { - case wasm::I32: - return std::to_string(value.get()); - case wasm::I64: - return std::to_string(value.get()); - case wasm::F32: - return std::to_string(value.get()); - case wasm::F64: - return std::to_string(value.get()); - default: - return "unknown"; - } -} - -static std::string printValues(const wasm::Val values[], size_t size) { - if (size == 0) { - return ""; - } - - std::string s; - for (size_t i = 0; i < size; i++) { - if (i) { - s.append(", "); - } - s.append(printValue(values[i])); - } - return s; -} - -static const char* printValKind(wasm::ValKind kind) { - switch (kind) { - case wasm::I32: - return "i32"; - case wasm::I64: - return "i64"; - case wasm::F32: - return "f32"; - case wasm::F64: - return "f64"; - case wasm::ANYREF: - return "anyref"; - case wasm::FUNCREF: - return "funcref"; - default: - return "unknown"; - } -} - -static std::string printValTypes(const wasm::ownvec& types) { - if (types.size() == 0) { - return "void"; - } - - std::string s; - s.reserve(types.size() * 8 /* max size + " " */ - 1); - for (size_t i = 0; i < types.size(); i++) { - if (i) { - s.append(" "); - } - s.append(printValKind(types[i]->kind())); - } - return s; -} - -static bool equalValTypes(const wasm::ownvec& left, - const wasm::ownvec& right) { - if (left.size() != right.size()) { - return false; - } - for (size_t i = 0; i < left.size(); i++) { - if (left[i]->kind() != right[i]->kind()) { - return false; - } - } - return true; -} - -static uint32_t parseVarint(const byte_t*& pos, const byte_t* end) { - uint32_t n = 0; - uint32_t shift = 0; - byte_t b; - do { - if (pos + 1 > end) { - throw WasmVmException("Failed to parse corrupted WASM module"); - } - b = *pos++; - n += (b & 0x7f) << shift; - shift += 7; - } while ((b & 0x80) != 0); - return n; -} - -// Template magic. - -template struct ConvertWordType { - using type = T; // NOLINT(readability-identifier-naming) -}; -template <> struct ConvertWordType { - using type = uint32_t; // NOLINT(readability-identifier-naming) -}; - -template wasm::Val makeVal(T t) { return wasm::Val::make(t); } -template <> wasm::Val makeVal(Word t) { return wasm::Val::make(static_cast(t.u64_)); } - -template constexpr auto convertArgToValKind(); -template <> constexpr auto convertArgToValKind() { return wasm::I32; }; -template <> constexpr auto convertArgToValKind() { return wasm::I32; }; -template <> constexpr auto convertArgToValKind() { return wasm::I32; }; -template <> constexpr auto convertArgToValKind() { return wasm::I64; }; -template <> constexpr auto convertArgToValKind() { return wasm::I64; }; -template <> constexpr auto convertArgToValKind() { return wasm::F32; }; -template <> constexpr auto convertArgToValKind() { return wasm::F64; }; - -template -constexpr auto convertArgsTupleToValTypesImpl(absl::index_sequence) { - return wasm::ownvec::make( - wasm::ValType::make(convertArgToValKind::type>())...); -} - -template constexpr auto convertArgsTupleToValTypes() { - return convertArgsTupleToValTypesImpl(absl::make_index_sequence::value>()); -} - -template -constexpr T convertValTypesToArgsTupleImpl(const U& arr, absl::index_sequence) { - return std::make_tuple( - (arr[I] - .template get< - typename ConvertWordType::type>::type>())...); -} - -template constexpr T convertValTypesToArgsTuple(const U& arr) { - return convertValTypesToArgsTupleImpl(arr, - absl::make_index_sequence::value>()); -} - -// V8 implementation. - -bool V8::load(const std::string& code, bool allow_precompiled) { - ENVOY_LOG(trace, "load()"); - store_ = wasm::Store::make(engine()); - - // Wasm file header is 8 bytes (magic number + version). - static const uint8_t magic_number[4] = {0x00, 0x61, 0x73, 0x6d}; - if (code.size() < 8 || ::memcmp(code.data(), magic_number, 4) != 0) { - return false; - } - - source_ = wasm::vec::make_uninitialized(code.size()); - ::memcpy(source_.get(), code.data(), code.size()); - - if (allow_precompiled) { - const auto section_name = getPrecompiledSectionName(); - if (!section_name.empty()) { - const auto precompiled = getCustomSection(section_name); - if (!precompiled.empty()) { - auto vec = wasm::vec::make_uninitialized(precompiled.size()); - ::memcpy(vec.get(), precompiled.data(), precompiled.size()); - - // TODO(PiotrSikora): fuzz loading of precompiled Wasm modules. - // See: https://github.com/envoyproxy/envoy/issues/9731 - module_ = wasm::Module::deserialize(store_.get(), vec); - if (!module_) { - // Precompiled module that cannot be loaded is considered a hard error, - // so don't fallback to compiling the bytecode. - return false; - } - } - } - } - - if (!module_) { - // TODO(PiotrSikora): fuzz loading of Wasm modules. - // See: https://github.com/envoyproxy/envoy/issues/9731 - const auto stripped_source = getStrippedSource(); - module_ = wasm::Module::make(store_.get(), stripped_source ? stripped_source : source_); - } - - if (module_) { - shared_module_ = module_->share(); - } - - return module_ != nullptr; -} - -WasmVmPtr V8::clone() { - ENVOY_LOG(trace, "clone()"); - ASSERT(shared_module_ != nullptr); - - auto clone = std::make_unique(scope_); - clone->store_ = wasm::Store::make(engine()); - - clone->module_ = wasm::Module::obtain(clone->store_.get(), shared_module_.get()); - - return clone; -} - -// Get Wasm module without Custom Sections to save some memory in workers. -wasm::vec V8::getStrippedSource() { - ENVOY_LOG(trace, "getStrippedSource()"); - ASSERT(source_.get() != nullptr); - - std::vector stripped; - - const byte_t* pos = source_.get() + 8 /* Wasm header */; - const byte_t* end = source_.get() + source_.size(); - while (pos < end) { - const auto section_start = pos; - if (pos + 1 > end) { - return wasm::vec::invalid(); - } - const auto section_type = *pos++; - const auto section_len = parseVarint(pos, end); - if (section_len == static_cast(-1) || pos + section_len > end) { - return wasm::vec::invalid(); - } - pos += section_len; - if (section_type == 0 /* custom section */) { - if (stripped.empty()) { - const byte_t* start = source_.get(); - stripped.insert(stripped.end(), start, section_start); - } - } else if (!stripped.empty()) { - stripped.insert(stripped.end(), section_start, pos /* section end */); - } - } - - // No custom sections found, use the original source. - if (stripped.empty()) { - return wasm::vec::invalid(); - } - - // Return stripped source, without custom sections. - return wasm::vec::make(stripped.size(), stripped.data()); -} - -absl::string_view V8::getCustomSection(absl::string_view name) { - ENVOY_LOG(trace, "getCustomSection(\"{}\")", name); - ASSERT(source_.get() != nullptr); - - const byte_t* pos = source_.get() + 8 /* Wasm header */; - const byte_t* end = source_.get() + source_.size(); - while (pos < end) { - if (pos + 1 > end) { - throw WasmVmException("Failed to parse corrupted WASM module"); - } - const auto section_type = *pos++; - const auto section_len = parseVarint(pos, end); - if (section_len == static_cast(-1) || pos + section_len > end) { - throw WasmVmException("Failed to parse corrupted WASM module"); - } - if (section_type == 0 /* custom section */) { - const auto section_data_start = pos; - const auto section_name_len = parseVarint(pos, end); - if (section_name_len == static_cast(-1) || pos + section_name_len > end) { - throw WasmVmException("Failed to parse corrupted WASM module"); - } - if (section_name_len == name.size() && ::memcmp(pos, name.data(), section_name_len) == 0) { - pos += section_name_len; - ENVOY_LOG(trace, "getCustomSection(\"{}\") found, size: {}", name, - section_data_start + section_len - pos); - return {pos, static_cast(section_data_start + section_len - pos)}; - } - pos = section_data_start + section_len; - } else { - pos += section_len; - } - } - return ""; -} - -#if defined(__linux__) && defined(__x86_64__) -#define WEE8_WASM_PRECOMPILE_PLATFORM "linux_x86_64" -#endif - -absl::string_view V8::getPrecompiledSectionName() { -#ifndef WEE8_WASM_PRECOMPILE_PLATFORM - return ""; -#else - static const auto name = - absl::StrCat("precompiled_v8_v", V8_MAJOR_VERSION, ".", V8_MINOR_VERSION, ".", - V8_BUILD_NUMBER, ".", V8_PATCH_LEVEL, "_", WEE8_WASM_PRECOMPILE_PLATFORM); - return name; -#endif -} - -void V8::link(absl::string_view debug_name) { - ENVOY_LOG(trace, "link(\"{}\")", debug_name); - ASSERT(module_ != nullptr); - - const auto import_types = module_.get()->imports(); - std::vector imports; - - for (size_t i = 0; i < import_types.size(); i++) { - absl::string_view module(import_types[i]->module().get(), import_types[i]->module().size()); - absl::string_view name(import_types[i]->name().get(), import_types[i]->name().size()); - auto import_type = import_types[i]->type(); - - switch (import_type->kind()) { - - case wasm::EXTERN_FUNC: { - ENVOY_LOG(trace, "link(), export host func: {}.{} ({} -> {})", module, name, - printValTypes(import_type->func()->params()), - printValTypes(import_type->func()->results())); - - auto it = host_functions_.find(absl::StrCat(module, ".", name)); - if (it == host_functions_.end()) { - throw WasmVmException( - fmt::format("Failed to load WASM module due to a missing import: {}.{}", module, name)); - } - auto func = it->second->callback_.get(); - if (!equalValTypes(import_type->func()->params(), func->type()->params()) || - !equalValTypes(import_type->func()->results(), func->type()->results())) { - throw WasmVmException(fmt::format( - "Failed to load WASM module due to an import type mismatch: {}.{}, " - "want: {} -> {}, but host exports: {} -> {}", - module, name, printValTypes(import_type->func()->params()), - printValTypes(import_type->func()->results()), printValTypes(func->type()->params()), - printValTypes(func->type()->results()))); - } - imports.push_back(func); - } break; - - case wasm::EXTERN_GLOBAL: { - // TODO(PiotrSikora): add support when/if needed. - ENVOY_LOG(trace, "link(), export host global: {}.{} ({})", module, name, - printValKind(import_type->global()->content()->kind())); - - throw WasmVmException( - fmt::format("Failed to load WASM module due to a missing import: {}.{}", module, name)); - } break; - - case wasm::EXTERN_MEMORY: { - ENVOY_LOG(trace, "link(), export host memory: {}.{} (min: {} max: {})", module, name, - import_type->memory()->limits().min, import_type->memory()->limits().max); - - ASSERT(memory_ == nullptr); - auto type = wasm::MemoryType::make(import_type->memory()->limits()); - memory_ = wasm::Memory::make(store_.get(), type.get()); - imports.push_back(memory_.get()); - } break; - - case wasm::EXTERN_TABLE: { - ENVOY_LOG(trace, "link(), export host table: {}.{} (min: {} max: {})", module, name, - import_type->table()->limits().min, import_type->table()->limits().max); - - ASSERT(table_ == nullptr); - auto type = - wasm::TableType::make(wasm::ValType::make(import_type->table()->element()->kind()), - import_type->table()->limits()); - table_ = wasm::Table::make(store_.get(), type.get()); - imports.push_back(table_.get()); - } break; - } - } - - ASSERT(import_types.size() == imports.size()); - - instance_ = wasm::Instance::make(store_.get(), module_.get(), imports.data()); - - const auto export_types = module_.get()->exports(); - const auto exports = instance_.get()->exports(); - ASSERT(export_types.size() == exports.size()); - - for (size_t i = 0; i < export_types.size(); i++) { - absl::string_view name(export_types[i]->name().get(), export_types[i]->name().size()); - auto export_type = export_types[i]->type(); - auto export_item = exports[i].get(); - ASSERT(export_type->kind() == export_item->kind()); - - switch (export_type->kind()) { - - case wasm::EXTERN_FUNC: { - ENVOY_LOG(trace, "link(), import module func: {} ({} -> {})", name, - printValTypes(export_type->func()->params()), - printValTypes(export_type->func()->results())); - - ASSERT(export_item->func() != nullptr); - module_functions_.insert_or_assign(name, export_item->func()->copy()); - } break; - - case wasm::EXTERN_GLOBAL: { - // TODO(PiotrSikora): add support when/if needed. - ENVOY_LOG(trace, "link(), import module global: {} ({}) --- IGNORED", name, - printValKind(export_type->global()->content()->kind())); - } break; - - case wasm::EXTERN_MEMORY: { - ENVOY_LOG(trace, "link(), import module memory: {} (min: {} max: {})", name, - export_type->memory()->limits().min, export_type->memory()->limits().max); - - ASSERT(export_item->memory() != nullptr); - ASSERT(memory_ == nullptr); - memory_ = exports[i]->memory()->copy(); - } break; - - case wasm::EXTERN_TABLE: { - // TODO(PiotrSikora): add support when/if needed. - ENVOY_LOG(trace, "link(), import module table: {} (min: {} max: {}) --- IGNORED", name, - export_type->table()->limits().min, export_type->table()->limits().max); - } break; - } - } -} - -uint64_t V8::getMemorySize() { - ENVOY_LOG(trace, "getMemorySize()"); - return memory_->data_size(); -} - -absl::optional V8::getMemory(uint64_t pointer, uint64_t size) { - ENVOY_LOG(trace, "getMemory({}, {})", pointer, size); - ASSERT(memory_ != nullptr); - if (pointer + size > memory_->data_size()) { - return absl::nullopt; - } - return absl::string_view(memory_->data() + pointer, size); -} - -bool V8::setMemory(uint64_t pointer, uint64_t size, const void* data) { - ENVOY_LOG(trace, "setMemory({}, {})", pointer, size); - ASSERT(memory_ != nullptr); - if (pointer + size > memory_->data_size()) { - return false; - } - ::memcpy(memory_->data() + pointer, data, size); - return true; -} - -bool V8::getWord(uint64_t pointer, Word* word) { - ENVOY_LOG(trace, "getWord({})", pointer); - constexpr auto size = sizeof(uint32_t); - if (pointer + size > memory_->data_size()) { - return false; - } - uint32_t word32; - ::memcpy(&word32, memory_->data() + pointer, size); - word->u64_ = word32; - return true; -} - -bool V8::setWord(uint64_t pointer, Word word) { - ENVOY_LOG(trace, "setWord({}, {})", pointer, word.u64_); - constexpr auto size = sizeof(uint32_t); - if (pointer + size > memory_->data_size()) { - return false; - } - uint32_t word32 = word.u32(); - ::memcpy(memory_->data() + pointer, &word32, size); - return true; -} - -template -void V8::registerHostFunctionImpl(absl::string_view module_name, absl::string_view function_name, - void (*function)(void*, Args...)) { - ENVOY_LOG(trace, "registerHostFunction(\"{}.{}\")", module_name, function_name); - auto data = std::make_unique(absl::StrCat(module_name, ".", function_name)); - auto type = wasm::FuncType::make(convertArgsTupleToValTypes>(), - convertArgsTupleToValTypes>()); - auto func = wasm::Func::make( - store_.get(), type.get(), - [](void* data, const wasm::Val params[], wasm::Val[]) -> wasm::own { - auto func_data = reinterpret_cast(data); - ENVOY_LOG(trace, "[vm->host] {}({})", func_data->name_, - printValues(params, std::tuple_size>::value)); - auto args_tuple = convertValTypesToArgsTuple>(params); - auto args = std::tuple_cat(std::make_tuple(current_context_), args_tuple); - auto function = reinterpret_cast(func_data->raw_func_); - absl::apply(function, args); - ENVOY_LOG(trace, "[vm<-host] {} return: void", func_data->name_); - return nullptr; - }, - data.get()); - data->callback_ = std::move(func); - data->raw_func_ = reinterpret_cast(function); - host_functions_.insert_or_assign(absl::StrCat(module_name, ".", function_name), std::move(data)); -} - -template -void V8::registerHostFunctionImpl(absl::string_view module_name, absl::string_view function_name, - R (*function)(void*, Args...)) { - ENVOY_LOG(trace, "registerHostFunction(\"{}.{}\")", module_name, function_name); - auto data = std::make_unique(absl::StrCat(module_name, ".", function_name)); - auto type = wasm::FuncType::make(convertArgsTupleToValTypes>(), - convertArgsTupleToValTypes>()); - auto func = wasm::Func::make( - store_.get(), type.get(), - [](void* data, const wasm::Val params[], wasm::Val results[]) -> wasm::own { - auto func_data = reinterpret_cast(data); - ENVOY_LOG(trace, "[vm->host] {}({})", func_data->name_, - printValues(params, sizeof...(Args))); - auto args_tuple = convertValTypesToArgsTuple>(params); - auto args = std::tuple_cat(std::make_tuple(current_context_), args_tuple); - auto function = reinterpret_cast(func_data->raw_func_); - R rvalue = absl::apply(function, args); - results[0] = makeVal(rvalue); - ENVOY_LOG(trace, "[vm<-host] {} return: {}", func_data->name_, rvalue); - return nullptr; - }, - data.get()); - data->callback_ = std::move(func); - data->raw_func_ = reinterpret_cast(function); - host_functions_.insert_or_assign(absl::StrCat(module_name, ".", function_name), std::move(data)); -} - -template -void V8::getModuleFunctionImpl(absl::string_view function_name, - std::function* function) { - ENVOY_LOG(trace, "getModuleFunction(\"{}\")", function_name); - auto it = module_functions_.find(function_name); - if (it == module_functions_.end()) { - *function = nullptr; - return; - } - const wasm::Func* func = it->second.get(); - if (!equalValTypes(func->type()->params(), convertArgsTupleToValTypes>()) || - !equalValTypes(func->type()->results(), convertArgsTupleToValTypes>())) { - throw WasmVmException(fmt::format("Bad function signature for: {}", function_name)); - } - *function = [func, function_name](Context* context, Args... args) -> void { - wasm::Val params[] = {makeVal(args)...}; - ENVOY_LOG(trace, "[host->vm] {}({})", function_name, printValues(params, sizeof...(Args))); - SaveRestoreContext saved_context(context); - auto trap = func->call(params, nullptr); - if (trap) { - throw WasmException( - fmt::format("Function: {} failed: {}", function_name, - absl::string_view(trap->message().get(), trap->message().size()))); - } - ENVOY_LOG(trace, "[host<-vm] {} return: void", function_name); - }; -} - -template -void V8::getModuleFunctionImpl(absl::string_view function_name, - std::function* function) { - ENVOY_LOG(trace, "getModuleFunction(\"{}\")", function_name); - auto it = module_functions_.find(function_name); - if (it == module_functions_.end()) { - *function = nullptr; - return; - } - const wasm::Func* func = it->second.get(); - if (!equalValTypes(func->type()->params(), convertArgsTupleToValTypes>()) || - !equalValTypes(func->type()->results(), convertArgsTupleToValTypes>())) { - throw WasmVmException(fmt::format("Bad function signature for: {}", function_name)); - } - *function = [func, function_name](Context* context, Args... args) -> R { - wasm::Val params[] = {makeVal(args)...}; - wasm::Val results[1]; - ENVOY_LOG(trace, "[host->vm] {}({})", function_name, printValues(params, sizeof...(Args))); - SaveRestoreContext saved_context(context); - auto trap = func->call(params, results); - if (trap) { - throw WasmException( - fmt::format("Function: {} failed: {}", function_name, - absl::string_view(trap->message().get(), trap->message().size()))); - } - R rvalue = results[0].get::type>(); - ENVOY_LOG(trace, "[host<-vm] {} return: {}", function_name, rvalue); - return rvalue; - }; -} - -WasmVmPtr createVm(const Stats::ScopeSharedPtr& scope) { return std::make_unique(scope); } - -} // namespace V8 -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/v8/v8.h b/source/extensions/common/wasm/v8/v8.h deleted file mode 100644 index a7288f0004a5..000000000000 --- a/source/extensions/common/wasm/v8/v8.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -#include "extensions/common/wasm/wasm_vm.h" - -namespace Envoy { -namespace Extensions { -namespace Common { -namespace Wasm { -namespace V8 { - -WasmVmPtr createVm(const Stats::ScopeSharedPtr& scope); - -} // namespace V8 -} // namespace Wasm -} // namespace Common -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm.cc b/source/extensions/common/wasm/wasm.cc new file mode 100644 index 000000000000..ab2a45f0aaf7 --- /dev/null +++ b/source/extensions/common/wasm/wasm.cc @@ -0,0 +1,489 @@ +#include "extensions/common/wasm/wasm.h" + +#include +#include + +#include "envoy/event/deferred_deletable.h" + +#include "common/common/logger.h" + +#include "extensions/common/wasm/wasm_extension.h" + +#include "absl/strings/str_cat.h" + +#define WASM_CONTEXT(_c) \ + static_cast(proxy_wasm::exports::ContextOrEffectiveContext( \ + static_cast((void)_c, proxy_wasm::current_context_))) + +using proxy_wasm::FailState; +using proxy_wasm::Word; + +namespace Envoy { + +using ScopeWeakPtr = std::weak_ptr; + +namespace Extensions { +namespace Common { +namespace Wasm { +namespace { + +using WasmEvent = EnvoyWasm::WasmEvent; + +struct CodeCacheEntry { + std::string code; + bool in_progress; + MonotonicTime use_time; + MonotonicTime fetch_time; +}; + +class RemoteDataFetcherAdapter : public Config::DataFetcher::RemoteDataFetcherCallback, + public Event::DeferredDeletable { +public: + RemoteDataFetcherAdapter(std::function cb) : cb_(cb) {} + ~RemoteDataFetcherAdapter() override = default; + void onSuccess(const std::string& data) override { cb_(data); } + void onFailure(Config::DataFetcher::FailureReason) override { cb_(""); } + void setFetcher(std::unique_ptr&& fetcher) { + fetcher_ = std::move(fetcher); + } + +private: + std::function cb_; + std::unique_ptr fetcher_; +}; + +const std::string INLINE_STRING = ""; +const int CODE_CACHE_SECONDS_NEGATIVE_CACHING = 10; +const int CODE_CACHE_SECONDS_CACHING_TTL = 24 * 3600; // 24 hours. +MonotonicTime::duration cache_time_offset_for_testing{}; + +std::atomic active_wasms; +std::mutex code_cache_mutex; +absl::flat_hash_map* code_cache = nullptr; + +// Downcast WasmBase to the actual Wasm. +inline Wasm* getWasm(WasmHandleSharedPtr& base_wasm_handle) { + return static_cast(base_wasm_handle->wasm().get()); +} + +} // namespace + +std::string anyToBytes(const ProtobufWkt::Any& any) { + if (any.Is()) { + ProtobufWkt::StringValue s; + MessageUtil::unpackTo(any, s); + return s.value(); + } + if (any.Is()) { + Protobuf::BytesValue b; + MessageUtil::unpackTo(any, b); + return b.value(); + } + return any.value(); +} + +void Wasm::initializeStats() { + active_wasms++; + wasm_stats_.active_.set(active_wasms); + wasm_stats_.created_.inc(); +} + +void Wasm::initializeLifecycle(Server::ServerLifecycleNotifier& lifecycle_notifier) { + auto weak = std::weak_ptr(std::static_pointer_cast(shared_from_this())); + lifecycle_notifier.registerCallback(Server::ServerLifecycleNotifier::Stage::ShutdownExit, + [this, weak](Event::PostCb post_cb) { + auto lock = weak.lock(); + if (lock) { // See if we are still alive. + server_shutdown_post_cb_ = post_cb; + } + }); +} + +Wasm::Wasm(absl::string_view runtime, absl::string_view vm_id, absl::string_view vm_configuration, + absl::string_view vm_key, const Stats::ScopeSharedPtr& scope, + Upstream::ClusterManager& cluster_manager, Event::Dispatcher& dispatcher) + : WasmBase(createWasmVm(runtime, scope), vm_id, vm_configuration, vm_key), scope_(scope), + cluster_manager_(cluster_manager), dispatcher_(dispatcher), + time_source_(dispatcher.timeSource()), + wasm_stats_(WasmStats{ + ALL_WASM_STATS(POOL_COUNTER_PREFIX(*scope_, absl::StrCat("wasm.", runtime, ".")), + POOL_GAUGE_PREFIX(*scope_, absl::StrCat("wasm.", runtime, ".")))}) { + initializeStats(); + ENVOY_LOG(debug, "Base Wasm created {} now active", active_wasms); +} + +Wasm::Wasm(WasmHandleSharedPtr base_wasm_handle, Event::Dispatcher& dispatcher) + : WasmBase(base_wasm_handle, + [&base_wasm_handle]() { + return createWasmVm( + getEnvoyWasmIntegration(*base_wasm_handle->wasm()->wasm_vm()).runtime(), + getWasm(base_wasm_handle)->scope_); + }), + scope_(getWasm(base_wasm_handle)->scope_), + cluster_manager_(getWasm(base_wasm_handle)->clusterManager()), dispatcher_(dispatcher), + time_source_(dispatcher.timeSource()), wasm_stats_(getWasm(base_wasm_handle)->wasm_stats_) { + initializeStats(); + ENVOY_LOG(debug, "Thread-Local Wasm created {} now active", active_wasms); +} + +void Wasm::error(absl::string_view message) { ENVOY_LOG(error, "Wasm VM failed {}", message); } + +void Wasm::setTimerPeriod(uint32_t context_id, std::chrono::milliseconds new_period) { + auto& period = timer_period_[context_id]; + auto& timer = timer_[context_id]; + bool was_running = timer && period.count() > 0; + period = new_period; + if (was_running) { + timer->disableTimer(); + } + if (period.count() > 0) { + timer = dispatcher_.createTimer( + [weak = std::weak_ptr(std::static_pointer_cast(shared_from_this())), + context_id]() { + auto shared = weak.lock(); + if (shared) { + shared->tickHandler(context_id); + } + }); + timer->enableTimer(period); + } +} + +void Wasm::tickHandler(uint32_t root_context_id) { + auto period = timer_period_.find(root_context_id); + auto timer = timer_.find(root_context_id); + if (period == timer_period_.end() || timer == timer_.end() || !on_tick_) { + return; + } + auto context = getContext(root_context_id); + if (context) { + context->onTick(0); + } + if (timer->second && period->second.count() > 0) { + timer->second->enableTimer(period->second); + } +} + +Wasm::~Wasm() { + active_wasms--; + wasm_stats_.active_.set(active_wasms); + ENVOY_LOG(debug, "~Wasm {} remaining active", active_wasms); + if (server_shutdown_post_cb_) { + dispatcher_.post(server_shutdown_post_cb_); + } +} + +// NOLINTNEXTLINE(readability-identifier-naming) +Word resolve_dns(void* raw_context, Word dns_address_ptr, Word dns_address_size, Word token_ptr) { + auto context = WASM_CONTEXT(raw_context); + auto root_context = context->isRootContext() ? context : context->rootContext(); + auto address = context->wasmVm()->getMemory(dns_address_ptr, dns_address_size); + if (!address) { + return WasmResult::InvalidMemoryAccess; + } + // Verify set and verify token_ptr before initiating the async resolve. + uint32_t token = context->wasm()->nextDnsToken(); + if (!context->wasm()->setDatatype(token_ptr, token)) { + return WasmResult::InvalidMemoryAccess; + } + auto callback = [weak_wasm = std::weak_ptr(context->wasm()->sharedThis()), root_context, + context_id = context->id(), + token](Envoy::Network::DnsResolver::ResolutionStatus status, + std::list&& response) { + auto wasm = weak_wasm.lock(); + if (!wasm) { + return; + } + root_context->onResolveDns(token, status, std::move(response)); + }; + if (!context->wasm()->dnsResolver()) { + context->wasm()->dnsResolver() = context->wasm()->dispatcher().createDnsResolver({}, false); + } + context->wasm()->dnsResolver()->resolve(std::string(address.value()), + Network::DnsLookupFamily::Auto, callback); + return WasmResult::Ok; +} + +void Wasm::registerCallbacks() { + WasmBase::registerCallbacks(); +#define _REGISTER(_fn) \ + wasm_vm_->registerCallback( \ + "env", "envoy_" #_fn, &_fn, \ + &proxy_wasm::ConvertFunctionWordToUint32::convertFunctionWordToUint32) + _REGISTER(resolve_dns); +#undef _REGISTER +} + +void Wasm::getFunctions() { + WasmBase::getFunctions(); +#define _GET(_fn) wasm_vm_->getFunction("envoy_" #_fn, &_fn##_); + _GET(on_resolve_dns) + _GET(on_stats_update) +#undef _GET +} + +proxy_wasm::CallOnThreadFunction Wasm::callOnThreadFunction() { + auto& dispatcher = dispatcher_; + return [&dispatcher](const std::function& f) { return dispatcher.post(f); }; +} + +ContextBase* Wasm::createContext(const std::shared_ptr& plugin) { + if (create_context_for_testing_) { + return create_context_for_testing_(this, std::static_pointer_cast(plugin)); + } + return new Context(this, std::static_pointer_cast(plugin)); +} + +ContextBase* Wasm::createRootContext(const std::shared_ptr& plugin) { + if (create_root_context_for_testing_) { + return create_root_context_for_testing_(this, std::static_pointer_cast(plugin)); + } + return new Context(this, std::static_pointer_cast(plugin)); +} + +ContextBase* Wasm::createVmContext() { return new Context(this); } + +void Wasm::log(absl::string_view root_id, const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap* response_headers, + const Http::ResponseTrailerMap* response_trailers, + const StreamInfo::StreamInfo& stream_info) { + auto context = getRootContext(root_id); + context->log(request_headers, response_headers, response_trailers, stream_info); +} + +void Wasm::onStatsUpdate(absl::string_view root_id, Envoy::Stats::MetricSnapshot& snapshot) { + auto context = getRootContext(root_id); + context->onStatsUpdate(snapshot); +} + +void clearCodeCacheForTesting() { + std::lock_guard guard(code_cache_mutex); + if (code_cache) { + delete code_cache; + code_cache = nullptr; + } + getWasmExtension()->resetStatsForTesting(); +} + +// TODO: remove this post #4160: Switch default to SimulatedTimeSystem. +void setTimeOffsetForCodeCacheForTesting(MonotonicTime::duration d) { + cache_time_offset_for_testing = d; +} + +static proxy_wasm::WasmHandleCloneFactory +getCloneFactory(WasmExtension* wasm_extension, Event::Dispatcher& dispatcher, + CreateContextFn create_root_context_for_testing) { + auto wasm_clone_factory = wasm_extension->wasmCloneFactory(); + return [&dispatcher, create_root_context_for_testing, wasm_clone_factory]( + WasmHandleBaseSharedPtr base_wasm) -> std::shared_ptr { + return wasm_clone_factory(std::static_pointer_cast(base_wasm), dispatcher, + create_root_context_for_testing); + }; +} + +WasmEvent toWasmEvent(const std::shared_ptr& wasm) { + if (!wasm) { + return WasmEvent::UnableToCreateVM; + } + switch (wasm->wasm()->fail_state()) { + case FailState::Ok: + return WasmEvent::Ok; + case FailState::UnableToCreateVM: + return WasmEvent::UnableToCreateVM; + case FailState::UnableToCloneVM: + return WasmEvent::UnableToCloneVM; + case FailState::MissingFunction: + return WasmEvent::MissingFunction; + case FailState::UnableToInitializeCode: + return WasmEvent::UnableToInitializeCode; + case FailState::StartFailed: + return WasmEvent::StartFailed; + case FailState::ConfigureFailed: + return WasmEvent::ConfigureFailed; + case FailState::RuntimeError: + return WasmEvent::RuntimeError; + } + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; +} + +static bool createWasmInternal(const VmConfig& vm_config, const PluginSharedPtr& plugin, + const Stats::ScopeSharedPtr& scope, + Upstream::ClusterManager& cluster_manager, + Init::Manager& init_manager, Event::Dispatcher& dispatcher, + Api::Api& api, Server::ServerLifecycleNotifier& lifecycle_notifier, + Config::DataSource::RemoteAsyncDataProviderPtr& remote_data_provider, + CreateWasmCallback&& cb, + CreateContextFn create_root_context_for_testing = nullptr) { + auto wasm_extension = getWasmExtension(); + std::string source, code; + bool fetch = false; + if (vm_config.code().has_remote()) { + auto now = dispatcher.timeSource().monotonicTime() + cache_time_offset_for_testing; + source = vm_config.code().remote().http_uri().uri(); + std::lock_guard guard(code_cache_mutex); + if (!code_cache) { + code_cache = new std::remove_reference::type; + } + Stats::ScopeSharedPtr create_wasm_stats_scope = + wasm_extension->lockAndCreateStats(scope, plugin); + // Remove entries older than CODE_CACHE_SECONDS_CACHING_TTL except for our target. + for (auto it = code_cache->begin(); it != code_cache->end();) { + if (now - it->second.use_time > std::chrono::seconds(CODE_CACHE_SECONDS_CACHING_TTL) && + it->first != vm_config.code().remote().sha256()) { + code_cache->erase(it++); + } else { + ++it; + } + } + wasm_extension->onRemoteCacheEntriesChanged(code_cache->size()); + auto it = code_cache->find(vm_config.code().remote().sha256()); + if (it != code_cache->end()) { + it->second.use_time = now; + if (it->second.in_progress) { + wasm_extension->onEvent(WasmExtension::WasmEvent::RemoteLoadCacheMiss, plugin); + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), warn, + "createWasm: failed to load (in progress) from {}", source); + cb(nullptr); + } + code = it->second.code; + if (code.empty()) { + if (now - it->second.fetch_time < + std::chrono::seconds(CODE_CACHE_SECONDS_NEGATIVE_CACHING)) { + wasm_extension->onEvent(WasmExtension::WasmEvent::RemoteLoadCacheNegativeHit, plugin); + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), warn, + "createWasm: failed to load (cached) from {}", source); + cb(nullptr); + } + fetch = true; // Fetch failed, retry. + it->second.in_progress = true; + it->second.fetch_time = now; + } else { + wasm_extension->onEvent(WasmExtension::WasmEvent::RemoteLoadCacheHit, plugin); + } + } else { + fetch = true; // Not in cache, fetch. + auto& e = (*code_cache)[vm_config.code().remote().sha256()]; + e.in_progress = true; + e.use_time = e.fetch_time = now; + wasm_extension->onRemoteCacheEntriesChanged(code_cache->size()); + wasm_extension->onEvent(WasmExtension::WasmEvent::RemoteLoadCacheMiss, plugin); + } + } else if (vm_config.code().has_local()) { + code = Config::DataSource::read(vm_config.code().local(), true, api); + source = Config::DataSource::getPath(vm_config.code().local()) + .value_or(code.empty() ? EMPTY_STRING : INLINE_STRING); + } + + auto complete_cb = [cb, vm_config, plugin, scope, &cluster_manager, &dispatcher, + &lifecycle_notifier, create_root_context_for_testing, + wasm_extension](std::string code) -> bool { + if (code.empty()) { + cb(nullptr); + return false; + } + auto vm_key = + proxy_wasm::makeVmKey(vm_config.vm_id(), anyToBytes(vm_config.configuration()), code); + auto wasm_factory = wasm_extension->wasmFactory(); + proxy_wasm::WasmHandleFactory proxy_wasm_factory = + [&vm_config, scope, &cluster_manager, &dispatcher, &lifecycle_notifier, + wasm_factory](absl::string_view vm_key) -> WasmHandleBaseSharedPtr { + return wasm_factory(vm_config, scope, cluster_manager, dispatcher, lifecycle_notifier, + vm_key); + }; + auto wasm = proxy_wasm::createWasm( + vm_key, code, plugin, proxy_wasm_factory, + getCloneFactory(wasm_extension, dispatcher, create_root_context_for_testing), + vm_config.allow_precompiled()); + Stats::ScopeSharedPtr create_wasm_stats_scope = + wasm_extension->lockAndCreateStats(scope, plugin); + wasm_extension->onEvent(toWasmEvent(wasm), plugin); + if (!wasm || wasm->wasm()->isFailed()) { + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), trace, + "Unable to create Wasm"); + cb(nullptr); + return false; + } + cb(std::static_pointer_cast(wasm)); + return true; + }; + + if (fetch) { + auto holder = std::make_shared>(); + auto fetch_callback = [vm_config, complete_cb, source, &dispatcher, scope, holder, plugin, + wasm_extension](const std::string& code) { + { + std::lock_guard guard(code_cache_mutex); + auto& e = (*code_cache)[vm_config.code().remote().sha256()]; + e.in_progress = false; + e.code = code; + Stats::ScopeSharedPtr create_wasm_stats_scope = + wasm_extension->lockAndCreateStats(scope, plugin); + if (code.empty()) { + wasm_extension->onEvent(WasmExtension::WasmEvent::RemoteLoadCacheFetchFailure, plugin); + } else { + wasm_extension->onEvent(WasmExtension::WasmEvent::RemoteLoadCacheFetchSuccess, plugin); + } + wasm_extension->onRemoteCacheEntriesChanged(code_cache->size()); + } + // NB: xDS currently does not support failing asynchronously, so we fail immediately + // if remote Wasm code is not cached and do a background fill. + if (!vm_config.nack_on_code_cache_miss()) { + if (code.empty()) { + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), trace, + "Failed to load Wasm code (fetch failed) from {}", source); + } + complete_cb(code); + } + // NB: must be deleted explicitly. + if (*holder) { + dispatcher.deferredDelete(Envoy::Event::DeferredDeletablePtr{holder->release()}); + } + }; + if (vm_config.nack_on_code_cache_miss()) { + auto adapter = std::make_unique(fetch_callback); + auto fetcher = std::make_unique( + cluster_manager, vm_config.code().remote().http_uri(), vm_config.code().remote().sha256(), + *adapter); + auto fetcher_ptr = fetcher.get(); + adapter->setFetcher(std::move(fetcher)); + *holder = std::move(adapter); + fetcher_ptr->fetch(); + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), trace, + fmt::format("Failed to load Wasm code (fetching) from {}", source)); + cb(nullptr); + return false; + } else { + remote_data_provider = std::make_unique( + cluster_manager, init_manager, vm_config.code().remote(), dispatcher, + api.randomGenerator(), true, fetch_callback); + } + } else { + return complete_cb(code); + } + return true; +} + +bool createWasm(const VmConfig& vm_config, const PluginSharedPtr& plugin, + const Stats::ScopeSharedPtr& scope, Upstream::ClusterManager& cluster_manager, + Init::Manager& init_manager, Event::Dispatcher& dispatcher, Api::Api& api, + Envoy::Server::ServerLifecycleNotifier& lifecycle_notifier, + Config::DataSource::RemoteAsyncDataProviderPtr& remote_data_provider, + CreateWasmCallback&& cb, CreateContextFn create_root_context_for_testing) { + return createWasmInternal(vm_config, plugin, scope, cluster_manager, init_manager, dispatcher, + api, lifecycle_notifier, remote_data_provider, std::move(cb), + create_root_context_for_testing); +} + +WasmHandleSharedPtr getOrCreateThreadLocalWasm(const WasmHandleSharedPtr& base_wasm, + const PluginSharedPtr& plugin, + Event::Dispatcher& dispatcher, + CreateContextFn create_root_context_for_testing) { + return std::static_pointer_cast(proxy_wasm::getOrCreateThreadLocalWasm( + std::static_pointer_cast(base_wasm), plugin, + getCloneFactory(getWasmExtension(), dispatcher, create_root_context_for_testing))); +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm.h b/source/extensions/common/wasm/wasm.h new file mode 100644 index 000000000000..a812d1a1a522 --- /dev/null +++ b/source/extensions/common/wasm/wasm.h @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/common/exception.h" +#include "envoy/extensions/wasm/v3/wasm.pb.validate.h" +#include "envoy/http/filter.h" +#include "envoy/server/lifecycle_notifier.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" +#include "envoy/thread_local/thread_local.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/assert.h" +#include "common/common/logger.h" +#include "common/config/datasource.h" +#include "common/stats/symbol_table_impl.h" +#include "common/version/version.h" + +#include "extensions/common/wasm/context.h" +#include "extensions/common/wasm/wasm_extension.h" +#include "extensions/common/wasm/wasm_vm.h" +#include "extensions/common/wasm/well_known_names.h" + +#include "include/proxy-wasm/exports.h" +#include "include/proxy-wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +#define ALL_WASM_STATS(COUNTER, GAUGE) \ + COUNTER(created) \ + GAUGE(active, NeverImport) + +class WasmHandle; + +struct WasmStats { + ALL_WASM_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) +}; + +// Wasm execution instance. Manages the Envoy side of the Wasm interface. +class Wasm : public WasmBase, Logger::Loggable { +public: + Wasm(absl::string_view runtime, absl::string_view vm_id, absl::string_view vm_configuration, + absl::string_view vm_key, const Stats::ScopeSharedPtr& scope, + Upstream::ClusterManager& cluster_manager, Event::Dispatcher& dispatcher); + Wasm(std::shared_ptr other, Event::Dispatcher& dispatcher); + ~Wasm() override; + + Upstream::ClusterManager& clusterManager() const { return cluster_manager_; } + Event::Dispatcher& dispatcher() { return dispatcher_; } + Context* getRootContext(absl::string_view root_id) { + return static_cast(WasmBase::getRootContext(root_id)); + } + void setTimerPeriod(uint32_t root_context_id, std::chrono::milliseconds period) override; + virtual void tickHandler(uint32_t root_context_id); + std::shared_ptr sharedThis() { return std::static_pointer_cast(shared_from_this()); } + Network::DnsResolverSharedPtr& dnsResolver() { return dns_resolver_; } + + // WasmBase + void error(absl::string_view message) override; + proxy_wasm::CallOnThreadFunction callOnThreadFunction() override; + ContextBase* createContext(const std::shared_ptr& plugin) override; + ContextBase* createRootContext(const std::shared_ptr& plugin) override; + ContextBase* createVmContext() override; + void registerCallbacks() override; + void getFunctions() override; + + // AccessLog::Instance + void log(absl::string_view root_id, const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap* response_headers, + const Http::ResponseTrailerMap* response_trailers, + const StreamInfo::StreamInfo& stream_info); + + void onStatsUpdate(absl::string_view root_id, Envoy::Stats::MetricSnapshot& snapshot); + virtual std::string buildVersion() { return BUILD_VERSION_NUMBER; } + + void initializeLifecycle(Server::ServerLifecycleNotifier& lifecycle_notifier); + uint32_t nextDnsToken() { + do { + dns_token_++; + } while (!dns_token_); + return dns_token_; + } + + void setCreateContextForTesting(CreateContextFn create_context, + CreateContextFn create_root_context) { + create_context_for_testing_ = create_context; + create_root_context_for_testing_ = create_root_context; + } + void setFailStateForTesting(proxy_wasm::FailState fail_state) { failed_ = fail_state; } + +protected: + friend class Context; + + void initializeStats(); + // Calls into the VM. + proxy_wasm::WasmCallVoid<3> on_resolve_dns_; + proxy_wasm::WasmCallVoid<2> on_stats_update_; + + Stats::ScopeSharedPtr scope_; + Upstream::ClusterManager& cluster_manager_; + Event::Dispatcher& dispatcher_; + Event::PostCb server_shutdown_post_cb_; + absl::flat_hash_map timer_; // per root_id. + TimeSource& time_source_; + + // Host Stats/Metrics + WasmStats wasm_stats_; + + // Plugin Stats/Metrics + absl::flat_hash_map counters_; + absl::flat_hash_map gauges_; + absl::flat_hash_map histograms_; + + CreateContextFn create_context_for_testing_; + CreateContextFn create_root_context_for_testing_; + Network::DnsResolverSharedPtr dns_resolver_; + uint32_t dns_token_ = 1; +}; +using WasmSharedPtr = std::shared_ptr; + +class WasmHandle : public WasmHandleBase, public ThreadLocal::ThreadLocalObject { +public: + explicit WasmHandle(const WasmSharedPtr& wasm) + : WasmHandleBase(std::static_pointer_cast(wasm)), wasm_(wasm) {} + + WasmSharedPtr& wasm() { return wasm_; } + +private: + WasmSharedPtr wasm_; +}; + +using CreateWasmCallback = std::function; + +// Returns false if createWasm failed synchronously. This is necessary because xDS *MUST* report +// all failures synchronously as it has no facility to report configuration update failures +// asynchronously. Callers should throw an exception if they are part of a synchronous xDS update +// because that is the mechanism for reporting configuration errors. +bool createWasm(const VmConfig& vm_config, const PluginSharedPtr& plugin, + const Stats::ScopeSharedPtr& scope, Upstream::ClusterManager& cluster_manager, + Init::Manager& init_manager, Event::Dispatcher& dispatcher, Api::Api& api, + Envoy::Server::ServerLifecycleNotifier& lifecycle_notifier, + Config::DataSource::RemoteAsyncDataProviderPtr& remote_data_provider, + CreateWasmCallback&& callback, + CreateContextFn create_root_context_for_testing = nullptr); + +WasmHandleSharedPtr +getOrCreateThreadLocalWasm(const WasmHandleSharedPtr& base_wasm, const PluginSharedPtr& plugin, + Event::Dispatcher& dispatcher, + CreateContextFn create_root_context_for_testing = nullptr); + +void clearCodeCacheForTesting(); +std::string anyToBytes(const ProtobufWkt::Any& any); +void setTimeOffsetForCodeCacheForTesting(MonotonicTime::duration d); +EnvoyWasm::WasmEvent toWasmEvent(const std::shared_ptr& wasm); + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_extension.cc b/source/extensions/common/wasm/wasm_extension.cc new file mode 100644 index 000000000000..c75168f1761c --- /dev/null +++ b/source/extensions/common/wasm/wasm_extension.cc @@ -0,0 +1,114 @@ +#include "extensions/common/wasm/wasm_extension.h" + +#include "extensions/common/wasm/context.h" +#include "extensions/common/wasm/wasm.h" +#include "extensions/common/wasm/wasm_vm.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace { + +WasmExtension* wasm_extension = nullptr; + +} // namespace + +Stats::ScopeSharedPtr WasmExtension::lockAndCreateStats(const Stats::ScopeSharedPtr& scope, + const PluginSharedPtr& plugin) { + absl::MutexLock l(&mutex_); + Stats::ScopeSharedPtr lock; + if (!(lock = scope_.lock())) { + resetStats(); + createStats(scope, plugin); + scope_ = ScopeWeakPtr(scope); + return scope; + } + createStats(scope, plugin); + return lock; +} + +void WasmExtension::resetStatsForTesting() { + absl::MutexLock l(&mutex_); + resetStats(); +} + +// Register a Wasm extension. Note: only one extension may be registered. +RegisterWasmExtension::RegisterWasmExtension(WasmExtension* extension) { + RELEASE_ASSERT(!wasm_extension, "Multiple Wasm extensions registered."); + wasm_extension = extension; +} + +std::unique_ptr +EnvoyWasm::createEnvoyWasmVmIntegration(const Stats::ScopeSharedPtr& scope, + absl::string_view runtime, + absl::string_view short_runtime) { + return std::make_unique(scope, runtime, short_runtime); +} + +WasmHandleExtensionFactory EnvoyWasm::wasmFactory() { + return [](const VmConfig vm_config, const Stats::ScopeSharedPtr& scope, + Upstream::ClusterManager& cluster_manager, Event::Dispatcher& dispatcher, + Server::ServerLifecycleNotifier& lifecycle_notifier, + absl::string_view vm_key) -> WasmHandleBaseSharedPtr { + auto wasm = std::make_shared(vm_config.runtime(), vm_config.vm_id(), + anyToBytes(vm_config.configuration()), vm_key, scope, + cluster_manager, dispatcher); + wasm->initializeLifecycle(lifecycle_notifier); + return std::static_pointer_cast(std::make_shared(std::move(wasm))); + }; +} + +WasmHandleExtensionCloneFactory EnvoyWasm::wasmCloneFactory() { + return [](const WasmHandleSharedPtr& base_wasm, Event::Dispatcher& dispatcher, + CreateContextFn create_root_context_for_testing) -> WasmHandleBaseSharedPtr { + auto wasm = std::make_shared(base_wasm, dispatcher); + wasm->setCreateContextForTesting(nullptr, create_root_context_for_testing); + return std::static_pointer_cast(std::make_shared(std::move(wasm))); + }; +} + +void EnvoyWasm::onEvent(WasmEvent event, const PluginSharedPtr&) { + switch (event) { + case WasmEvent::RemoteLoadCacheHit: + create_wasm_stats_->remote_load_cache_hits_.inc(); + break; + case WasmEvent::RemoteLoadCacheNegativeHit: + create_wasm_stats_->remote_load_cache_negative_hits_.inc(); + break; + case WasmEvent::RemoteLoadCacheMiss: + create_wasm_stats_->remote_load_cache_misses_.inc(); + break; + case WasmEvent::RemoteLoadCacheFetchSuccess: + create_wasm_stats_->remote_load_fetch_successes_.inc(); + break; + case WasmEvent::RemoteLoadCacheFetchFailure: + create_wasm_stats_->remote_load_fetch_failures_.inc(); + break; + default: + break; + } +} + +void EnvoyWasm::onRemoteCacheEntriesChanged(int entries) { + create_wasm_stats_->remote_load_cache_entries_.set(entries); +} + +void EnvoyWasm::createStats(const Stats::ScopeSharedPtr& scope, const PluginSharedPtr&) { + if (!create_wasm_stats_) { + create_wasm_stats_.reset(new CreateWasmStats{CREATE_WASM_STATS( // NOLINT + POOL_COUNTER_PREFIX(*scope, "wasm."), POOL_GAUGE_PREFIX(*scope, "wasm."))}); + } +} + +void EnvoyWasm::resetStats() { create_wasm_stats_.reset(); } + +WasmExtension* getWasmExtension() { + static WasmExtension* extension = wasm_extension ? wasm_extension : new EnvoyWasm(); + return extension; +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_extension.h b/source/extensions/common/wasm/wasm_extension.h new file mode 100644 index 000000000000..5d41a58bb337 --- /dev/null +++ b/source/extensions/common/wasm/wasm_extension.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include "envoy/server/lifecycle_notifier.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/logger.h" +#include "common/stats/symbol_table_impl.h" + +#include "extensions/common/wasm/context.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +#define CREATE_WASM_STATS(COUNTER, GAUGE) \ + COUNTER(remote_load_cache_hits) \ + COUNTER(remote_load_cache_negative_hits) \ + COUNTER(remote_load_cache_misses) \ + COUNTER(remote_load_fetch_successes) \ + COUNTER(remote_load_fetch_failures) \ + GAUGE(remote_load_cache_entries, NeverImport) + +class WasmHandle; +class EnvoyWasmVmIntegration; + +using WasmHandleSharedPtr = std::shared_ptr; +using CreateContextFn = + std::function& plugin)>; +using WasmHandleExtensionFactory = std::function; +using WasmHandleExtensionCloneFactory = std::function; +using ScopeWeakPtr = std::weak_ptr; + +struct CreateWasmStats { + CREATE_WASM_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) +}; + +// Extension point for Wasm clients in embedded Envoy. +class WasmExtension : Logger::Loggable { +public: + WasmExtension() = default; + virtual ~WasmExtension() = default; + + virtual void initialize() = 0; + virtual std::unique_ptr + createEnvoyWasmVmIntegration(const Stats::ScopeSharedPtr& scope, absl::string_view runtime, + absl::string_view short_runtime) = 0; + virtual WasmHandleExtensionFactory wasmFactory() = 0; + virtual WasmHandleExtensionCloneFactory wasmCloneFactory() = 0; + enum class WasmEvent : int { + Ok, + RemoteLoadCacheHit, + RemoteLoadCacheNegativeHit, + RemoteLoadCacheMiss, + RemoteLoadCacheFetchSuccess, + RemoteLoadCacheFetchFailure, + UnableToCreateVM, + UnableToCloneVM, + MissingFunction, + UnableToInitializeCode, + StartFailed, + ConfigureFailed, + RuntimeError, + }; + virtual void onEvent(WasmEvent event, const PluginSharedPtr& plugin) = 0; + virtual void onRemoteCacheEntriesChanged(int remote_cache_entries) = 0; + virtual void createStats(const Stats::ScopeSharedPtr& scope, const PluginSharedPtr& plugin) + EXCLUSIVE_LOCKS_REQUIRED(mutex_) = 0; + virtual void resetStats() EXCLUSIVE_LOCKS_REQUIRED(mutex_) = 0; // Delete stats pointers + + // NB: the Scope can become invalid if, for example, the owning FilterChain is deleted. When that + // happens the stats must be recreated. This hook verifies the Scope of any existing stats and if + // necessary recreates the stats with the newly provided scope. + // This call takes out the mutex_ and calls createStats and possibly resetStats(). + Stats::ScopeSharedPtr lockAndCreateStats(const Stats::ScopeSharedPtr& scope, + const PluginSharedPtr& plugin); + + void resetStatsForTesting(); + +protected: + absl::Mutex mutex_; + ScopeWeakPtr scope_; +}; + +// The default Envoy Wasm implementation. +class EnvoyWasm : public WasmExtension { +public: + EnvoyWasm() = default; + ~EnvoyWasm() override = default; + void initialize() override {} + std::unique_ptr + createEnvoyWasmVmIntegration(const Stats::ScopeSharedPtr& scope, absl::string_view runtime, + absl::string_view short_runtime) override; + WasmHandleExtensionFactory wasmFactory() override; + WasmHandleExtensionCloneFactory wasmCloneFactory() override; + void onEvent(WasmEvent event, const PluginSharedPtr& plugin) override; + void onRemoteCacheEntriesChanged(int remote_cache_entries) override; + void createStats(const Stats::ScopeSharedPtr& scope, const PluginSharedPtr& plugin) override; + void resetStats() override; + +private: + std::unique_ptr create_wasm_stats_; +}; + +// Register a Wasm extension. Note: only one extension may be registered. +struct RegisterWasmExtension { + RegisterWasmExtension(WasmExtension* extension); +}; +#define REGISTER_WASM_EXTENSION(_class) \ + ::Envoy::Extensions::Common::Wasm::RegisterWasmExtension register_wasm_extension(new _class()); + +WasmExtension* getWasmExtension(); + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_state.cc b/source/extensions/common/wasm/wasm_state.cc new file mode 100644 index 000000000000..573523f1d83e --- /dev/null +++ b/source/extensions/common/wasm/wasm_state.cc @@ -0,0 +1,59 @@ +#include "extensions/common/wasm/wasm_state.h" + +#include "flatbuffers/reflection.h" +#include "tools/flatbuffers_backed_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +using google::api::expr::runtime::CelValue; + +CelValue WasmState::exprValue(Protobuf::Arena* arena, bool last) const { + if (initialized_) { + switch (type_) { + case WasmType::String: + return CelValue::CreateString(&value_); + case WasmType::Bytes: + return CelValue::CreateBytes(&value_); + case WasmType::Protobuf: { + if (last) { + return CelValue::CreateBytes(&value_); + } + // Note that this is very expensive since it incurs a de-serialization + const auto any = serializeAsProto(); + return CelValue::CreateMessage(any.get(), arena); + } + case WasmType::FlatBuffers: + if (last) { + return CelValue::CreateBytes(&value_); + } + return CelValue::CreateMap(google::api::expr::runtime::CreateFlatBuffersBackedObject( + reinterpret_cast(value_.data()), *reflection::GetSchema(schema_.data()), + arena)); + } + } + return CelValue::CreateNull(); +} + +ProtobufTypes::MessagePtr WasmState::serializeAsProto() const { + auto any = std::make_unique(); + + if (type_ != WasmType::Protobuf) { + ProtobufWkt::BytesValue value; + value.set_value(value_); + any->PackFrom(value); + } else { + // The Wasm extension serialized in its own type. + any->set_type_url(std::string(schema_)); + any->set_value(value_); + } + + return any; +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_state.h b/source/extensions/common/wasm/wasm_state.h new file mode 100644 index 000000000000..ee0371550f36 --- /dev/null +++ b/source/extensions/common/wasm/wasm_state.h @@ -0,0 +1,87 @@ +/* + * Wasm State Class available to Wasm/Non-Wasm modules. + */ + +#pragma once + +#include + +#include "envoy/stream_info/filter_state.h" + +#include "common/protobuf/protobuf.h" +#include "common/singleton/const_singleton.h" + +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "eval/public/cel_value.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +// FilterState prefix for WasmState values. +const absl::string_view WasmStateKeyPrefix = "wasm."; + +// WasmState content declaration. +enum class WasmType { + Bytes, + String, + // Schema contains the reflection flatbuffer + FlatBuffers, + // Schema contains the type URL + Protobuf, +}; + +// WasmState type declaration. +class WasmStatePrototype { +public: + WasmStatePrototype(bool readonly, WasmType type, absl::string_view schema, + StreamInfo::FilterState::LifeSpan life_span) + : readonly_(readonly), type_(type), schema_(schema), life_span_(life_span) {} + WasmStatePrototype() = default; + const bool readonly_{false}; + const WasmType type_{WasmType::Bytes}; + const std::string schema_{""}; + const StreamInfo::FilterState::LifeSpan life_span_{ + StreamInfo::FilterState::LifeSpan::FilterChain}; +}; + +using DefaultWasmStatePrototype = ConstSingleton; + +// A simple wrapper around generic values +class WasmState : public StreamInfo::FilterState::Object { +public: + explicit WasmState(const WasmStatePrototype& proto) + : readonly_(proto.readonly_), type_(proto.type_), schema_(proto.schema_) {} + + const std::string& value() const { return value_; } + + // Create a value from the state, given an arena. Last argument indicates whether the value + // is de-referenced. + google::api::expr::runtime::CelValue exprValue(Protobuf::Arena* arena, bool last) const; + + bool setValue(absl::string_view value) { + if (initialized_ && readonly_) { + return false; + } + value_.assign(value.data(), value.size()); + initialized_ = true; + return true; + } + + ProtobufTypes::MessagePtr serializeAsProto() const override; + absl::optional serializeAsString() const override { return value_; } + +private: + const bool readonly_; + const WasmType type_; + absl::string_view schema_; + std::string value_{}; + bool initialized_{false}; +}; + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_vm.cc b/source/extensions/common/wasm/wasm_vm.cc index 9299eceba2d1..9f888b18f8f5 100644 --- a/source/extensions/common/wasm/wasm_vm.cc +++ b/source/extensions/common/wasm/wasm_vm.cc @@ -1,30 +1,94 @@ #include "extensions/common/wasm/wasm_vm.h" +#include #include -#include "extensions/common/wasm/null/null.h" -#include "extensions/common/wasm/v8/v8.h" +#include "extensions/common/wasm/context.h" +#include "extensions/common/wasm/ext/envoy_null_vm_wasm_api.h" +#include "extensions/common/wasm/wasm_extension.h" #include "extensions/common/wasm/well_known_names.h" +#include "include/proxy-wasm/null.h" +#include "include/proxy-wasm/null_plugin.h" + +#if defined(ENVOY_WASM_V8) +#include "include/proxy-wasm/v8.h" +#endif +#if defined(ENVOY_WASM_WAVM) +#include "include/proxy-wasm/wavm.h" +#endif + +using ContextBase = proxy_wasm::ContextBase; +using Word = proxy_wasm::Word; + namespace Envoy { namespace Extensions { namespace Common { namespace Wasm { -thread_local Envoy::Extensions::Common::Wasm::Context* current_context_ = nullptr; -thread_local uint32_t effective_context_id_ = 0; +void EnvoyWasmVmIntegration::error(absl::string_view message) { ENVOY_LOG(trace, message); } + +bool EnvoyWasmVmIntegration::getNullVmFunction(absl::string_view function_name, bool returns_word, + int number_of_arguments, + proxy_wasm::NullPlugin* plugin, + void* ptr_to_function_return) { + if (function_name == "envoy_on_resolve_dns" && returns_word == false && + number_of_arguments == 3) { + *reinterpret_cast*>(ptr_to_function_return) = + [plugin](ContextBase* context, Word context_id, Word token, Word result_size) { + proxy_wasm::SaveRestoreContext saved_context(context); + // Need to add a new API header available to both .wasm and null vm targets. + auto context_base = plugin->getContextBase(context_id); + if (auto root = context_base->asRoot()) { + static_cast(root)->onResolveDns( + token, result_size); + } + }; + return true; + } else if (function_name == "envoy_on_stats_update" && returns_word == false && + number_of_arguments == 2) { + *reinterpret_cast*>( + ptr_to_function_return) = [plugin](ContextBase* context, Word context_id, + Word result_size) { + proxy_wasm::SaveRestoreContext saved_context(context); + // Need to add a new API header available to both .wasm and null vm targets. + auto context_base = plugin->getContextBase(context_id); + if (auto root = context_base->asRoot()) { + static_cast(root)->onStatsUpdate(result_size); + } + }; + return true; + } + return false; +} WasmVmPtr createWasmVm(absl::string_view runtime, const Stats::ScopeSharedPtr& scope) { if (runtime.empty()) { - throw WasmVmException("Failed to create WASM VM with unspecified runtime."); + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), warn, + "Failed to create Wasm VM with unspecified runtime"); + return nullptr; } else if (runtime == WasmRuntimeNames::get().Null) { - return Null::createVm(scope); + auto wasm = proxy_wasm::createNullVm(); + wasm->integration() = getWasmExtension()->createEnvoyWasmVmIntegration(scope, runtime, "null"); + return wasm; +#if defined(ENVOY_WASM_V8) } else if (runtime == WasmRuntimeNames::get().V8) { - return V8::createVm(scope); + auto wasm = proxy_wasm::createV8Vm(); + wasm->integration() = getWasmExtension()->createEnvoyWasmVmIntegration(scope, runtime, "v8"); + return wasm; +#endif +#if defined(ENVOY_WASM_WAVM) + } else if (runtime == WasmRuntimeNames::get().Wavm) { + auto wasm = proxy_wasm::createWavmVm(); + wasm->integration() = getWasmExtension()->createEnvoyWasmVmIntegration(scope, runtime, "wavm"); + return wasm; +#endif } else { - throw WasmVmException(fmt::format( - "Failed to create WASM VM using {} runtime. Envoy was compiled without support for it.", - runtime)); + ENVOY_LOG_TO_LOGGER( + Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), warn, + "Failed to create Wasm VM using {} runtime. Envoy was compiled without support for it", + runtime); + return nullptr; } } diff --git a/source/extensions/common/wasm/wasm_vm.h b/source/extensions/common/wasm/wasm_vm.h index 3506eaaa0966..0099e63d1144 100644 --- a/source/extensions/common/wasm/wasm_vm.h +++ b/source/extensions/common/wasm/wasm_vm.h @@ -4,266 +4,73 @@ #include "envoy/common/exception.h" #include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" +#include "envoy/stats/stats_macros.h" #include "common/common/logger.h" -#include "absl/types/optional.h" +#include "absl/strings/str_cat.h" +#include "include/proxy-wasm/wasm_vm.h" +#include "include/proxy-wasm/word.h" namespace Envoy { namespace Extensions { namespace Common { namespace Wasm { -class Context; +/** + * Wasm host stats. + */ +#define ALL_VM_STATS(COUNTER, GAUGE) \ + COUNTER(created) \ + COUNTER(cloned) \ + GAUGE(active, NeverImport) -// Represents a WASM-native word-sized datum. On 32-bit VMs, the high bits are always zero. -// The WASM/VM API treats all bits as significant. -struct Word { - Word(uint64_t w) : u64_(w) {} // Implicit conversion into Word. - uint32_t u32() const { return static_cast(u64_); } - uint64_t u64_; +struct VmStats { + ALL_VM_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) }; -inline std::ostream& operator<<(std::ostream& os, const Word& w) { return os << w.u64_; } - -// Convert Word type for use by 32-bit VMs. -template struct ConvertWordTypeToUint32 { - using type = T; // NOLINT(readability-identifier-naming) -}; -template <> struct ConvertWordTypeToUint32 { - using type = uint32_t; // NOLINT(readability-identifier-naming) -}; - -// Convert Word-based function types for 32-bit VMs. -template struct ConvertFunctionTypeWordToUint32 {}; -template struct ConvertFunctionTypeWordToUint32 { - using type = typename ConvertWordTypeToUint32::type (*)( - typename ConvertWordTypeToUint32::type...); -}; - -template inline auto convertWordToUint32(T t) { return t; } -template <> inline auto convertWordToUint32(Word t) { return static_cast(t.u64_); } - -// Convert a function of the form Word(Word...) to one of the form uint32_t(uint32_t...). -template struct ConvertFunctionWordToUint32 { - static void convertFunctionWordToUint32() {} -}; -template R> -struct ConvertFunctionWordToUint32 { - static typename ConvertWordTypeToUint32::type - convertFunctionWordToUint32(typename ConvertWordTypeToUint32::type... args) { - return convertWordToUint32(F(std::forward(args)...)); +// Wasm VM data providing stats. +class EnvoyWasmVmIntegration : public proxy_wasm::WasmVmIntegration, + Logger::Loggable { +public: + EnvoyWasmVmIntegration(const Stats::ScopeSharedPtr& scope, absl::string_view runtime, + absl::string_view short_runtime) + : scope_(scope), runtime_(std::string(runtime)), short_runtime_(std::string(short_runtime)), + runtime_prefix_(absl::StrCat("wasm_vm.", short_runtime, ".")), + stats_(VmStats{ALL_VM_STATS(POOL_COUNTER_PREFIX(*scope_, runtime_prefix_), + POOL_GAUGE_PREFIX(*scope_, runtime_prefix_))}) { + stats_.created_.inc(); + stats_.active_.inc(); + ENVOY_LOG(debug, "WasmVm created {} now active", runtime_, stats_.active_.value()); } -}; -template void> -struct ConvertFunctionWordToUint32 { - static void convertFunctionWordToUint32(typename ConvertWordTypeToUint32::type... args) { - F(std::forward(args)...); + ~EnvoyWasmVmIntegration() override { + stats_.active_.dec(); + ENVOY_LOG(debug, "~WasmVm {} {} remaining active", runtime_, stats_.active_.value()); } -}; - -#define CONVERT_FUNCTION_WORD_TO_UINT32(_f) \ - &ConvertFunctionWordToUint32::convertFunctionWordToUint32 - -// These are templates and its helper for constructing signatures of functions calling into and out -// of WASM VMs. -// - WasmFuncTypeHelper is a helper for WasmFuncType and shouldn't be used anywhere else than -// WasmFuncType definition. -// - WasmFuncType takes 4 template parameter which are number of argument, return type, context type -// and param type respectively, resolve to a function type. -// For example `WasmFuncType<3, void, Context*, Word>` resolves to `void(Context*, Word, Word, -// Word)` -template -struct WasmFuncTypeHelper {}; - -template -struct WasmFuncTypeHelper { - // NOLINTNEXTLINE(readability-identifier-naming) - using type = typename WasmFuncTypeHelper::type; -}; - -template -struct WasmFuncTypeHelper<0, ReturnType, ContextType, ParamType, ReturnType(ContextType, Args...)> { - using type = ReturnType(ContextType, Args...); // NOLINT(readability-identifier-naming) -}; - -template -using WasmFuncType = typename WasmFuncTypeHelper::type; - -// Calls into the WASM VM. -// 1st arg is always a pointer to Context (Context*). -template using WasmCallVoid = std::function>; -template using WasmCallWord = std::function>; - -#define FOR_ALL_WASM_VM_EXPORTS(_f) \ - _f(WasmCallVoid<0>) _f(WasmCallVoid<1>) _f(WasmCallVoid<2>) _f(WasmCallVoid<3>) \ - _f(WasmCallVoid<5>) _f(WasmCallWord<1>) _f(WasmCallWord<2>) _f(WasmCallWord<3>) - -// Calls out of the WASM VM. -// 1st arg is always a pointer to raw_context (void*). -template using WasmCallbackVoid = WasmFuncType*; -template using WasmCallbackWord = WasmFuncType*; - -// Using the standard g++/clang mangling algorithm: -// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling-builtin -// Extended with W = Word -// Z = void, j = uint32_t, l = int64_t, m = uint64_t -using WasmCallback_WWl = Word (*)(void*, Word, int64_t); -using WasmCallback_WWlWW = Word (*)(void*, Word, int64_t, Word, Word); -using WasmCallback_WWm = Word (*)(void*, Word, uint64_t); -using WasmCallback_dd = double (*)(void*, double); - -#define FOR_ALL_WASM_VM_IMPORTS(_f) \ - _f(WasmCallbackVoid<0>) _f(WasmCallbackVoid<1>) _f(WasmCallbackVoid<2>) _f(WasmCallbackVoid<3>) \ - _f(WasmCallbackVoid<4>) _f(WasmCallbackWord<0>) _f(WasmCallbackWord<1>) \ - _f(WasmCallbackWord<2>) _f(WasmCallbackWord<3>) _f(WasmCallbackWord<4>) \ - _f(WasmCallbackWord<5>) _f(WasmCallbackWord<6>) _f(WasmCallbackWord<7>) \ - _f(WasmCallbackWord<8>) _f(WasmCallbackWord<9>) _f(WasmCallbackWord<10>) \ - _f(WasmCallback_WWl) _f(WasmCallback_WWlWW) _f(WasmCallback_WWm) \ - _f(WasmCallback_dd) - -enum class Cloneable { - NotCloneable, // VMs can not be cloned and should be created from scratch. - CompiledBytecode, // VMs can be cloned with compiled bytecode. - InstantiatedModule // VMs can be cloned from an instantiated module. -}; - -// Wasm VM instance. Provides the low level WASM interface. -class WasmVm : public Logger::Loggable { -public: - using WasmVmPtr = std::unique_ptr; - - virtual ~WasmVm() = default; - /** - * Return the runtime identifier. - * @return one of WasmRuntimeValues from well_known_names.h (e.g. "envoy.wasm.runtime.null"). - */ - virtual absl::string_view runtime() PURE; - - /** - * Whether or not the VM implementation supports cloning. Cloning is VM system dependent. - * When a VM is configured a single VM is instantiated to check that the .wasm file is valid and - * to do VM system specific initialization. In the case of WAVM this is potentially ahead-of-time - * compilation. Then, if cloning is supported, we clone that VM for each worker, potentially - * copying and sharing the initialized data structures for efficiency. Otherwise we create an new - * VM from scratch for each worker. - * @return one of enum Cloneable with the VMs cloneability. - */ - virtual Cloneable cloneable() PURE; - - /** - * Make a worker/thread-specific copy if supported by the underlying VM system (see cloneable() - * above). If not supported, the caller will need to create a new VM from scratch. If supported, - * the clone may share compiled code and other read-only data with the source VM. - * @return a clone of 'this' (e.g. for a different worker/thread). - */ - virtual WasmVmPtr clone() PURE; - - /** - * Load the WASM code from a file. Return true on success. Once the module is loaded it can be - * queried, e.g. to see which version of emscripten support is required. After loading, the - * appropriate ABI callbacks can be registered and then the module can be link()ed (see below). - * @param code the WASM binary code (or registered NullVm plugin name). - * @param allow_precompiled if true, allows supporting VMs (e.g. WAVM) to load the binary - * machine code from a user-defined section of the WASM file. Because that code is not verified by - * the envoy process it is up to the user to ensure that the code is both safe and is built for - * the linked in version of WAVM. - * @return whether or not the load was successful. - */ - virtual bool load(const std::string& code, bool allow_precompiled) PURE; - - /** - * Link the WASM code to the host-provided functions, e.g. the ABI. Prior to linking, the module - * should be loaded and the ABI callbacks registered (see above). Linking should be done once - * after load(). - * @param debug_name user-provided name for use in log and error messages. - */ - virtual void link(absl::string_view debug_name) PURE; - - /** - * Get size of the currently allocated memory in the VM. - * @return the size of memory in bytes. - */ - virtual uint64_t getMemorySize() PURE; - - /** - * Convert a block of memory in the VM to a string_view. - * @param pointer the offset into VM memory of the requested VM memory block. - * @param size the size of the requested VM memory block. - * @return if std::nullopt then the pointer/size pair were invalid, otherwise returns - * a host string_view pointing to the pointer/size pair in VM memory. - */ - virtual absl::optional getMemory(uint64_t pointer, uint64_t size) PURE; - - /** - * Set a block of memory in the VM, returns true on success, false if the pointer/size is invalid. - * @param pointer the offset into VM memory describing the start of a region of VM memory. - * @param size the size of the region of VM memory. - * @return whether or not the pointer/size pair was a valid VM memory block. - */ - virtual bool setMemory(uint64_t pointer, uint64_t size, const void* data) PURE; - - /** - * Get a VM native Word (e.g. sizeof(void*) or sizeof(size_t)) from VM memory, returns true on - * success, false if the pointer is invalid. WASM-32 VMs have 32-bit native words and WASM-64 VMs - * (not yet supported) will have 64-bit words as does the Null VM (compiled into 64-bit Envoy). - * This function can be used to chase pointers in VM memory. - * @param pointer the offset into VM memory describing the start of VM native word size block. - * @param data a pointer to a Word whose contents will be filled from the VM native word at - * 'pointer'. - * @return whether or not the pointer was to a valid VM memory block of VM native word size. - */ - virtual bool getWord(uint64_t pointer, Word* data) PURE; - /** - * Set a Word in the VM, returns true on success, false if the pointer is invalid. - * See getWord above for details. This function can be used (for example) to set indirect pointer - * return values (e.g. proxy_getHeaderHapValue(... const char** value_ptr, size_t* value_size). - * @param pointer the offset into VM memory describing the start of VM native word size block. - * @param data a Word whose contents will be written in VM native word size at 'pointer'. - * @return whether or not the pointer was to a valid VM memory block of VM native word size. - */ - virtual bool setWord(uint64_t pointer, Word data) PURE; - - /** - * Get the contents of the custom section with the given name or "" if it does not exist. - * @param name the name of the custom section to get. - * @return the contents of the custom section (if any). The result will be empty if there - * is no such section. - */ - virtual absl::string_view getCustomSection(absl::string_view name) PURE; - - /** - * Get the name of the custom section that contains precompiled module. - * @return the name of the custom section that contains precompiled module. - */ - virtual absl::string_view getPrecompiledSectionName() PURE; + // proxy_wasm::WasmVmIntegration + proxy_wasm::WasmVmIntegration* clone() override { + return new EnvoyWasmVmIntegration(scope_, runtime_, short_runtime_); + } + bool getNullVmFunction(absl::string_view function_name, bool returns_word, + int number_of_arguments, proxy_wasm::NullPlugin* plugin, + void* ptr_to_function_return) override; + void error(absl::string_view message) override; - /** - * Get typed function exported by the WASM module. - */ -#define _GET_FUNCTION(_T) virtual void getFunction(absl::string_view function_name, _T* f) PURE; - FOR_ALL_WASM_VM_EXPORTS(_GET_FUNCTION) -#undef _GET_FUNCTION + const std::string& runtime() const { return runtime_; } - /** - * Register typed callbacks exported by the host environment. - */ -#define _REGISTER_CALLBACK(_T) \ - virtual void registerCallback(absl::string_view moduleName, absl::string_view function_name, \ - _T f, typename ConvertFunctionTypeWordToUint32<_T>::type) PURE; - FOR_ALL_WASM_VM_IMPORTS(_REGISTER_CALLBACK) -#undef _REGISTER_CALLBACK -}; -using WasmVmPtr = std::unique_ptr; +protected: + const Stats::ScopeSharedPtr scope_; + const std::string runtime_; + const std::string short_runtime_; + const std::string runtime_prefix_; + VmStats stats_; +}; // namespace Wasm -// Exceptions for issues with the WasmVm. -class WasmVmException : public EnvoyException { -public: - using EnvoyException::EnvoyException; -}; +inline EnvoyWasmVmIntegration& getEnvoyWasmIntegration(proxy_wasm::WasmVm& wasm_vm) { + return *static_cast(wasm_vm.integration().get()); +} // Exceptions for issues with the WebAssembly code. class WasmException : public EnvoyException { @@ -271,36 +78,9 @@ class WasmException : public EnvoyException { using EnvoyException::EnvoyException; }; -// Thread local state set during a call into a WASM VM so that calls coming out of the -// VM can be attributed correctly to calling Filter. We use thread_local instead of ThreadLocal -// because this state is live only during the calls and does not need to be initialized consistently -// over all workers as with ThreadLocal data. -extern thread_local Envoy::Extensions::Common::Wasm::Context* current_context_; - -// Requested effective context set by code within the VM to request that the calls coming out of the -// VM be attributed to another filter, for example if a control plane gRPC comes back to the -// RootContext which effects some set of waiting filters. -extern thread_local uint32_t effective_context_id_; - -// Helper to save and restore thread local VM call context information to support reentrant calls. -// NB: this happens for example when a call from the VM invokes a handler which needs to _malloc -// memory in the VM. -struct SaveRestoreContext { - explicit SaveRestoreContext(Context* context) { - saved_context = current_context_; - saved_effective_context_id_ = effective_context_id_; - current_context_ = context; - effective_context_id_ = 0; // No effective context id. - } - ~SaveRestoreContext() { - current_context_ = saved_context; - effective_context_id_ = saved_effective_context_id_; - } - Context* saved_context; - uint32_t saved_effective_context_id_; -}; +using WasmVmPtr = std::unique_ptr; -// Create a new low-level WASM VM using runtime of the given type (e.g. "envoy.wasm.runtime.wavm"). +// Create a new low-level Wasm VM using runtime of the given type (e.g. "envoy.wasm.runtime.wavm"). WasmVmPtr createWasmVm(absl::string_view runtime, const Stats::ScopeSharedPtr& scope); } // namespace Wasm diff --git a/source/extensions/common/wasm/well_known_names.h b/source/extensions/common/wasm/well_known_names.h index 3fb39ed54a4d..5fb8602bf831 100644 --- a/source/extensions/common/wasm/well_known_names.h +++ b/source/extensions/common/wasm/well_known_names.h @@ -15,11 +15,16 @@ namespace Wasm { */ class WasmRuntimeValues { public: + // WAVM (https://github.com/WAVM/WAVM) Wasm VM. + const std::string Wavm = "envoy.wasm.runtime.wavm"; // Null sandbox: modules must be compiled into envoy and registered name is given in the // DataSource.inline_string. const std::string Null = "envoy.wasm.runtime.null"; // V8-based (https://v8.dev) WebAssembly runtime. const std::string V8 = "envoy.wasm.runtime.v8"; + + // Filter state name + const std::string FilterState = "envoy.wasm"; }; using WasmRuntimeNames = ConstSingleton; diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 111292995a01..ddc3dc9a0d50 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -7,6 +7,7 @@ EXTENSIONS = { "envoy.access_loggers.file": "//source/extensions/access_loggers/file:config", "envoy.access_loggers.http_grpc": "//source/extensions/access_loggers/grpc:http_config", "envoy.access_loggers.tcp_grpc": "//source/extensions/access_loggers/grpc:tcp_config", + "envoy.access_loggers.wasm": "//source/extensions/access_loggers/wasm:config", # # Clusters @@ -30,6 +31,11 @@ EXTENSIONS = { "envoy.grpc_credentials.file_based_metadata": "//source/extensions/grpc_credentials/file_based_metadata:config", "envoy.grpc_credentials.aws_iam": "//source/extensions/grpc_credentials/aws_iam:config", + # + # WASM + # + "envoy.bootstrap.wasm": "//source/extensions/bootstrap/wasm:config", + # # Health checkers # @@ -75,6 +81,7 @@ EXTENSIONS = { "envoy.filters.http.router": "//source/extensions/filters/http/router:config", "envoy.filters.http.squash": "//source/extensions/filters/http/squash:config", "envoy.filters.http.tap": "//source/extensions/filters/http/tap:config", + "envoy.filters.http.wasm": "//source/extensions/filters/http/wasm:config", # # Listener filters @@ -114,6 +121,7 @@ EXTENSIONS = { "envoy.filters.network.thrift_proxy": "//source/extensions/filters/network/thrift_proxy:config", "envoy.filters.network.sni_cluster": "//source/extensions/filters/network/sni_cluster:config", "envoy.filters.network.sni_dynamic_forward_proxy": "//source/extensions/filters/network/sni_dynamic_forward_proxy:config", + "envoy.filters.network.wasm": "//source/extensions/filters/network/wasm:config", "envoy.filters.network.zookeeper_proxy": "//source/extensions/filters/network/zookeeper_proxy:config", # @@ -138,6 +146,7 @@ EXTENSIONS = { "envoy.stat_sinks.hystrix": "//source/extensions/stat_sinks/hystrix:config", "envoy.stat_sinks.metrics_service": "//source/extensions/stat_sinks/metrics_service:config", "envoy.stat_sinks.statsd": "//source/extensions/stat_sinks/statsd:config", + "envoy.stat_sinks.wasm": "//source/extensions/stat_sinks/wasm:config", # # Thrift filters diff --git a/source/extensions/filters/http/cache/cache_headers_utils.cc b/source/extensions/filters/http/cache/cache_headers_utils.cc index f94e5506725e..b00a46bd37fe 100644 --- a/source/extensions/filters/http/cache/cache_headers_utils.cc +++ b/source/extensions/filters/http/cache/cache_headers_utils.cc @@ -184,7 +184,7 @@ absl::optional CacheHeadersUtils::readAndRemoveLeadingDigits(absl::str } uint64_t new_val = (val * 10) + (cur - '0'); if (new_val / 8 < val) { - // Overflow occurred. + // Overflow occurred return absl::nullopt; } val = new_val; @@ -192,7 +192,7 @@ absl::optional CacheHeadersUtils::readAndRemoveLeadingDigits(absl::str } if (bytes_consumed) { - // Consume some digits. + // Consume some digits str.remove_prefix(bytes_consumed); return val; } diff --git a/source/extensions/filters/http/wasm/BUILD b/source/extensions/filters/http/wasm/BUILD new file mode 100644 index 000000000000..81d0a69665e1 --- /dev/null +++ b/source/extensions/filters/http/wasm/BUILD @@ -0,0 +1,45 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +# Public docs: docs/root/configuration/http_filters/wasm_filter.rst + +envoy_cc_library( + name = "wasm_filter_lib", + srcs = ["wasm_filter.cc"], + hdrs = ["wasm_filter.h"], + visibility = ["//visibility:public"], + deps = [ + "//include/envoy/http:codes_interface", + "//include/envoy/server:filter_config_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/filters/http:well_known_names", + "@envoy_api//envoy/extensions/filters/http/wasm/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "unknown", + status = "alpha", + deps = [ + ":wasm_filter_lib", + "//include/envoy/registry", + "//source/common/common:empty_string", + "//source/common/config:datasource_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/wasm/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/wasm/config.cc b/source/extensions/filters/http/wasm/config.cc new file mode 100644 index 000000000000..f46b7cf0692f --- /dev/null +++ b/source/extensions/filters/http/wasm/config.cc @@ -0,0 +1,39 @@ +#include "extensions/filters/http/wasm/config.h" + +#include "envoy/extensions/filters/http/wasm/v3/wasm.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/common/empty_string.h" +#include "common/config/datasource.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/http/wasm/wasm_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Wasm { + +Http::FilterFactoryCb WasmFilterConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::wasm::v3::Wasm& proto_config, const std::string&, + Server::Configuration::FactoryContext& context) { + auto filter_config = std::make_shared(proto_config, context); + return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + auto filter = filter_config->createFilter(); + if (!filter) { // Fail open + return; + } + callbacks.addStreamFilter(filter); + callbacks.addAccessLogHandler(filter); + }; +} + +/** + * Static registration for the Wasm filter. @see RegisterFactory. + */ +REGISTER_FACTORY(WasmFilterConfig, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace Wasm +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/wasm/config.h b/source/extensions/filters/http/wasm/config.h new file mode 100644 index 000000000000..319aee96f9ca --- /dev/null +++ b/source/extensions/filters/http/wasm/config.h @@ -0,0 +1,31 @@ +#pragma once + +#include "envoy/extensions/filters/http/wasm/v3/wasm.pb.h" +#include "envoy/extensions/filters/http/wasm/v3/wasm.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Wasm { + +/** + * Config registration for the Wasm filter. @see NamedHttpFilterConfigFactory. + */ +class WasmFilterConfig + : public Common::FactoryBase { +public: + WasmFilterConfig() : FactoryBase(HttpFilterNames::get().Wasm) {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::wasm::v3::Wasm& proto_config, const std::string&, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace Wasm +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/wasm/wasm_filter.cc b/source/extensions/filters/http/wasm/wasm_filter.cc new file mode 100644 index 000000000000..c62b06c4102d --- /dev/null +++ b/source/extensions/filters/http/wasm/wasm_filter.cc @@ -0,0 +1,52 @@ +#include "extensions/filters/http/wasm/wasm_filter.h" + +#include "envoy/http/codes.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/assert.h" +#include "common/common/enum_to_int.h" +#include "common/http/header_map_impl.h" +#include "common/http/message_impl.h" +#include "common/http/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Wasm { + +FilterConfig::FilterConfig(const envoy::extensions::filters::http::wasm::v3::Wasm& config, + Server::Configuration::FactoryContext& context) + : tls_slot_(context.threadLocal().allocateSlot()) { + plugin_ = std::make_shared( + config.config().name(), config.config().root_id(), config.config().vm_config().vm_id(), + config.config().vm_config().runtime(), + Common::Wasm::anyToBytes(config.config().configuration()), config.config().fail_open(), + context.direction(), context.localInfo(), &context.listenerMetadata()); + + auto plugin = plugin_; + auto callback = [plugin, this](const Common::Wasm::WasmHandleSharedPtr& base_wasm) { + // NB: the Slot set() call doesn't complete inline, so all arguments must outlive this call. + tls_slot_->set( + [base_wasm, + plugin](Event::Dispatcher& dispatcher) -> std::shared_ptr { + if (!base_wasm) { + return nullptr; + } + return std::static_pointer_cast( + Common::Wasm::getOrCreateThreadLocalWasm(base_wasm, plugin, dispatcher)); + }); + }; + + if (!Common::Wasm::createWasm( + config.config().vm_config(), plugin_, context.scope().createScope(""), + context.clusterManager(), context.initManager(), context.dispatcher(), context.api(), + context.lifecycleNotifier(), remote_data_provider_, std::move(callback))) { + throw Common::Wasm::WasmException( + fmt::format("Unable to create Wasm HTTP filter {}", plugin->name_)); + } +} + +} // namespace Wasm +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/wasm/wasm_filter.h b/source/extensions/filters/http/wasm/wasm_filter.h new file mode 100644 index 000000000000..36bfd1503b77 --- /dev/null +++ b/source/extensions/filters/http/wasm/wasm_filter.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "envoy/extensions/filters/http/wasm/v3/wasm.pb.validate.h" +#include "envoy/http/filter.h" +#include "envoy/server/filter_config.h" +#include "envoy/upstream/cluster_manager.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Wasm { + +using Envoy::Extensions::Common::Wasm::Context; +using Envoy::Extensions::Common::Wasm::Wasm; +using Envoy::Extensions::Common::Wasm::WasmHandle; + +class FilterConfig : Logger::Loggable { +public: + FilterConfig(const envoy::extensions::filters::http::wasm::v3::Wasm& proto_config, + Server::Configuration::FactoryContext& context); + + std::shared_ptr createFilter() { + Wasm* wasm = nullptr; + if (tls_slot_->get()) { + wasm = tls_slot_->getTyped().wasm().get(); + } + if (plugin_->fail_open_ && (!wasm || wasm->isFailed())) { + return nullptr; + } + if (wasm && !root_context_id_) { + root_context_id_ = wasm->getRootContext(plugin_->root_id_)->id(); + } + return std::make_shared(wasm, root_context_id_, plugin_); + } + +private: + uint32_t root_context_id_{0}; + Envoy::Extensions::Common::Wasm::PluginSharedPtr plugin_; + ThreadLocal::SlotPtr tls_slot_; + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +typedef std::shared_ptr FilterConfigSharedPtr; + +} // namespace Wasm +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index 6402d09d96bc..dc331ef8e3df 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -74,6 +74,8 @@ class HttpFilterNameValues { const std::string OriginalSrc = "envoy.filters.http.original_src"; // Dynamic forward proxy filter const std::string DynamicForwardProxy = "envoy.filters.http.dynamic_forward_proxy"; + // WebAssembly filter + const std::string Wasm = "envoy.filters.http.wasm"; // AWS request signing filter const std::string AwsRequestSigning = "envoy.filters.http.aws_request_signing"; // AWS Lambda filter diff --git a/source/extensions/filters/network/wasm/BUILD b/source/extensions/filters/network/wasm/BUILD new file mode 100644 index 000000000000..f87909482665 --- /dev/null +++ b/source/extensions/filters/network/wasm/BUILD @@ -0,0 +1,43 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +# Public docs: docs/root/configuration/network_filters/wasm_filter.rst + +envoy_cc_library( + name = "wasm_filter_lib", + srcs = ["wasm_filter.cc"], + hdrs = ["wasm_filter.h"], + deps = [ + "//include/envoy/server:filter_config_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/filters/network:well_known_names", + "@envoy_api//envoy/extensions/filters/network/wasm/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "unknown", + status = "alpha", + deps = [ + ":wasm_filter_lib", + "//include/envoy/registry", + "//source/common/common:empty_string", + "//source/common/config:datasource_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/network/wasm/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/network/wasm/config.cc b/source/extensions/filters/network/wasm/config.cc new file mode 100644 index 000000000000..05f8f1abb854 --- /dev/null +++ b/source/extensions/filters/network/wasm/config.cc @@ -0,0 +1,37 @@ +#include "extensions/filters/network/wasm/config.h" + +#include "envoy/extensions/filters/network/wasm/v3/wasm.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/common/empty_string.h" +#include "common/config/datasource.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/network/wasm/wasm_filter.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace Wasm { + +Network::FilterFactoryCb WasmFilterConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::wasm::v3::Wasm& proto_config, + Server::Configuration::FactoryContext& context) { + auto filter_config = std::make_shared(proto_config, context); + return [filter_config](Network::FilterManager& filter_manager) -> void { + auto filter = filter_config->createFilter(); + if (filter) { + filter_manager.addFilter(filter); + } // else fail open + }; +} + +/** + * Static registration for the Wasm filter. @see RegisterFactory. + */ +REGISTER_FACTORY(WasmFilterConfig, Server::Configuration::NamedNetworkFilterConfigFactory); + +} // namespace Wasm +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/wasm/config.h b/source/extensions/filters/network/wasm/config.h new file mode 100644 index 000000000000..12201c56b187 --- /dev/null +++ b/source/extensions/filters/network/wasm/config.h @@ -0,0 +1,31 @@ +#pragma once + +#include "envoy/extensions/filters/network/wasm/v3/wasm.pb.h" +#include "envoy/extensions/filters/network/wasm/v3/wasm.pb.validate.h" + +#include "extensions/filters/network/common/factory_base.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace Wasm { + +/** + * Config registration for the Wasm filter. @see NamedNetworkFilterConfigFactory. + */ +class WasmFilterConfig + : public Common::FactoryBase { +public: + WasmFilterConfig() : FactoryBase(NetworkFilterNames::get().Wasm) {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::wasm::v3::Wasm& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace Wasm +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/wasm/wasm_filter.cc b/source/extensions/filters/network/wasm/wasm_filter.cc new file mode 100644 index 000000000000..9d253b675abd --- /dev/null +++ b/source/extensions/filters/network/wasm/wasm_filter.cc @@ -0,0 +1,47 @@ +#include "extensions/filters/network/wasm/wasm_filter.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/assert.h" +#include "common/common/enum_to_int.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace Wasm { + +FilterConfig::FilterConfig(const envoy::extensions::filters::network::wasm::v3::Wasm& config, + Server::Configuration::FactoryContext& context) + : tls_slot_(context.threadLocal().allocateSlot()) { + plugin_ = std::make_shared( + config.config().name(), config.config().root_id(), config.config().vm_config().vm_id(), + config.config().vm_config().runtime(), + Common::Wasm::anyToBytes(config.config().configuration()), config.config().fail_open(), + context.direction(), context.localInfo(), &context.listenerMetadata()); + + auto plugin = plugin_; + auto callback = [plugin, this](Common::Wasm::WasmHandleSharedPtr base_wasm) { + // NB: the Slot set() call doesn't complete inline, so all arguments must outlive this call. + tls_slot_->set( + [base_wasm, + plugin](Event::Dispatcher& dispatcher) -> std::shared_ptr { + if (!base_wasm) { + return nullptr; + } + return std::static_pointer_cast( + Common::Wasm::getOrCreateThreadLocalWasm(base_wasm, plugin, dispatcher)); + }); + }; + + if (!Common::Wasm::createWasm( + config.config().vm_config(), plugin_, context.scope().createScope(""), + context.clusterManager(), context.initManager(), context.dispatcher(), context.api(), + context.lifecycleNotifier(), remote_data_provider_, std::move(callback))) { + throw Common::Wasm::WasmException( + fmt::format("Unable to create Wasm network filter {}", plugin->name_)); + } +} + +} // namespace Wasm +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/wasm/wasm_filter.h b/source/extensions/filters/network/wasm/wasm_filter.h new file mode 100644 index 000000000000..51adbcd7ac0c --- /dev/null +++ b/source/extensions/filters/network/wasm/wasm_filter.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include "envoy/extensions/filters/network/wasm/v3/wasm.pb.validate.h" +#include "envoy/network/filter.h" +#include "envoy/server/filter_config.h" +#include "envoy/upstream/cluster_manager.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace Wasm { + +using Envoy::Extensions::Common::Wasm::Context; +using Envoy::Extensions::Common::Wasm::Wasm; +using Envoy::Extensions::Common::Wasm::WasmHandle; + +class FilterConfig : Logger::Loggable { +public: + FilterConfig(const envoy::extensions::filters::network::wasm::v3::Wasm& proto_config, + Server::Configuration::FactoryContext& context); + + std::shared_ptr createFilter() { + Wasm* wasm = nullptr; + if (tls_slot_->get()) { + wasm = tls_slot_->getTyped().wasm().get(); + } + if (plugin_->fail_open_ && (!wasm || wasm->isFailed())) { + return nullptr; + } + if (wasm && !root_context_id_) { + root_context_id_ = wasm->getRootContext(plugin_->root_id_)->id(); + } + return std::make_shared(wasm, root_context_id_, plugin_); + } + Envoy::Extensions::Common::Wasm::Wasm* wasm() { + return tls_slot_->getTyped().wasm().get(); + } + +private: + uint32_t root_context_id_{0}; + Envoy::Extensions::Common::Wasm::PluginSharedPtr plugin_; + ThreadLocal::SlotPtr tls_slot_; + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +typedef std::shared_ptr FilterConfigSharedPtr; + +} // namespace Wasm +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index 78564a5a990f..7f62193c7896 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -52,6 +52,8 @@ class NetworkFilterNameValues { const std::string SniDynamicForwardProxy = "envoy.filters.network.sni_dynamic_forward_proxy"; // ZooKeeper proxy filter const std::string ZooKeeperProxy = "envoy.filters.network.zookeeper_proxy"; + // WebAssembly filter + const std::string Wasm = "envoy.filters.network.wasm"; }; using NetworkFilterNames = ConstSingleton; diff --git a/source/extensions/stat_sinks/wasm/BUILD b/source/extensions/stat_sinks/wasm/BUILD new file mode 100644 index 000000000000..70e156ac4acc --- /dev/null +++ b/source/extensions/stat_sinks/wasm/BUILD @@ -0,0 +1,39 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +# Stats sink for wasm + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "data_plane_agnostic", + status = "alpha", + deps = [ + ":wasm_stat_sink_lib", + "//include/envoy/registry", + "//include/envoy/server:factory_context_interface", + "//include/envoy/server:instance_interface", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/stat_sinks:well_known_names", + "//source/server:configuration_lib", + "@envoy_api//envoy/extensions/stat_sinks/wasm/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "wasm_stat_sink_lib", + hdrs = ["wasm_stat_sink_impl.h"], + deps = [ + "//include/envoy/stats:stats_interface", + "//source/extensions/common/wasm:wasm_lib", + ], +) diff --git a/source/extensions/stat_sinks/wasm/config.cc b/source/extensions/stat_sinks/wasm/config.cc new file mode 100644 index 000000000000..ba94937a3b3a --- /dev/null +++ b/source/extensions/stat_sinks/wasm/config.cc @@ -0,0 +1,71 @@ +#include "extensions/stat_sinks/wasm/config.h" + +#include + +#include "envoy/extensions/stat_sinks/wasm/v3/wasm.pb.validate.h" +#include "envoy/registry/registry.h" +#include "envoy/server/factory_context.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/stat_sinks/wasm/wasm_stat_sink_impl.h" +#include "extensions/stat_sinks/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace Wasm { + +Stats::SinkPtr +WasmSinkFactory::createStatsSink(const Protobuf::Message& proto_config, + Server::Configuration::ServerFactoryContext& context) { + const auto& config = + MessageUtil::downcastAndValidate( + proto_config, context.messageValidationContext().staticValidationVisitor()); + + auto wasm_sink = std::make_unique(config.config().root_id(), nullptr); + + auto plugin = std::make_shared( + config.config().name(), config.config().root_id(), config.config().vm_config().vm_id(), + config.config().vm_config().runtime(), + Common::Wasm::anyToBytes(config.config().configuration()), config.config().fail_open(), + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, context.localInfo(), nullptr); + + auto callback = [&wasm_sink, &context, plugin](Common::Wasm::WasmHandleSharedPtr base_wasm) { + if (!base_wasm) { + if (plugin->fail_open_) { + ENVOY_LOG(error, "Unable to create Wasm Stat Sink {}", plugin->name_); + } else { + ENVOY_LOG(critical, "Unable to create Wasm Stat Sink {}", plugin->name_); + } + return; + } + wasm_sink->setSingleton( + Common::Wasm::getOrCreateThreadLocalWasm(base_wasm, plugin, context.dispatcher())); + }; + + if (!Common::Wasm::createWasm( + config.config().vm_config(), plugin, context.scope().createScope(""), + context.clusterManager(), context.initManager(), context.dispatcher(), context.api(), + context.lifecycleNotifier(), remote_data_provider_, std::move(callback))) { + throw Common::Wasm::WasmException( + fmt::format("Unable to create Wasm Stat Sink {}", plugin->name_)); + } + + return wasm_sink; +} + +ProtobufTypes::MessagePtr WasmSinkFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +std::string WasmSinkFactory::name() const { return StatsSinkNames::get().Wasm; } + +/** + * Static registration for the wasm access log. @see RegisterFactory. + */ +REGISTER_FACTORY(WasmSinkFactory, Server::Configuration::StatsSinkFactory); + +} // namespace Wasm +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/stat_sinks/wasm/config.h b/source/extensions/stat_sinks/wasm/config.h new file mode 100644 index 000000000000..4fbd3d72937a --- /dev/null +++ b/source/extensions/stat_sinks/wasm/config.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "envoy/server/factory_context.h" +#include "envoy/server/instance.h" + +#include "common/config/datasource.h" + +#include "server/configuration_impl.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace Wasm { + +/** + * Config registration for the Wasm statsd sink. @see StatSinkFactory. + */ +class WasmSinkFactory : Logger::Loggable, + public Server::Configuration::StatsSinkFactory { +public: + // StatsSinkFactory + Stats::SinkPtr createStatsSink(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() const override; + +private: + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +} // namespace Wasm +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/stat_sinks/wasm/wasm_stat_sink_impl.h b/source/extensions/stat_sinks/wasm/wasm_stat_sink_impl.h new file mode 100644 index 000000000000..5f2a9b6e13f9 --- /dev/null +++ b/source/extensions/stat_sinks/wasm/wasm_stat_sink_impl.h @@ -0,0 +1,41 @@ +#pragma once + +#include "envoy/stats/sink.h" + +#include "extensions/common/wasm/wasm.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace Wasm { + +using Envoy::Extensions::Common::Wasm::WasmHandle; + +class WasmStatSink : public Stats::Sink { +public: + WasmStatSink(absl::string_view root_id, Common::Wasm::WasmHandleSharedPtr singleton) + : root_id_(root_id), singleton_(std::move(singleton)) {} + + void flush(Stats::MetricSnapshot& snapshot) override { + singleton_->wasm()->onStatsUpdate(root_id_, snapshot); + } + + void setSingleton(Common::Wasm::WasmHandleSharedPtr singleton) { + ASSERT(singleton != nullptr); + singleton_ = std::move(singleton); + } + + void onHistogramComplete(const Stats::Histogram& histogram, uint64_t value) override { + (void)histogram; + (void)value; + } + +private: + std::string root_id_; + Common::Wasm::WasmHandleSharedPtr singleton_; +}; + +} // namespace Wasm +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/stat_sinks/well_known_names.h b/source/extensions/stat_sinks/well_known_names.h index afb16a0a4baf..a1f5c7965d03 100644 --- a/source/extensions/stat_sinks/well_known_names.h +++ b/source/extensions/stat_sinks/well_known_names.h @@ -22,6 +22,8 @@ class StatsSinkNameValues { const std::string MetricsService = "envoy.stat_sinks.metrics_service"; // Hystrix sink const std::string Hystrix = "envoy.stat_sinks.hystrix"; + // WebAssembly sink + const std::string Wasm = "envoy.stat_sinks.wasm"; }; using StatsSinkNames = ConstSingleton; diff --git a/test/common/network/connection_impl_test.cc b/test/common/network/connection_impl_test.cc index 47ba64c2bbb1..5724c30425f2 100644 --- a/test/common/network/connection_impl_test.cc +++ b/test/common/network/connection_impl_test.cc @@ -132,9 +132,30 @@ class ConnectionImplTest : public testing::TestWithParam { Network::Test::getCanonicalLoopbackAddress(GetParam()), nullptr, true); listener_ = dispatcher_->createListener(socket_, listener_callbacks_, true, ENVOY_TCP_BACKLOG_SIZE); +#if defined(__clang__) && defined(__has_feature) && __has_feature(address_sanitizer) + // There is a bug in clang with AddressSanitizer on the CI such that the code below reports: + // + // runtime error: constructor call on address 0x6190000b4a80 with insufficient space for + // an object of type 'Envoy::Network::(anonymous namespace)::TestClientConnectionImpl' + // 0x6190000b4a80: note: pointer points here + // 05 01 80 39 be be be be be be be be be be be be be be be be be be be be be be be be + // be be be be + // + // However, the workaround below trips gcc on the CI, which reports: + // + // size check failed 2304 1280 38 + // CorrectSize(p, size, tcmalloc::DefaultAlignPolicy()) + // + // so we only use it for clang with AddressSanitizer builds. + auto x = malloc(sizeof(TestClientConnectionImpl) + 1024); + new (x) TestClientConnectionImpl(*dispatcher_, socket_->localAddress(), source_address_, + Network::Test::createRawBufferSocket(), socket_options_); + client_connection_.reset(reinterpret_cast(x)); +#else client_connection_ = std::make_unique( *dispatcher_, socket_->localAddress(), source_address_, Network::Test::createRawBufferSocket(), socket_options_); +#endif client_connection_->addConnectionCallbacks(client_callbacks_); EXPECT_EQ(nullptr, client_connection_->ssl()); const Network::ClientConnection& const_connection = *client_connection_; diff --git a/test/extensions/access_loggers/wasm/BUILD b/test/extensions/access_loggers/wasm/BUILD new file mode 100644 index 000000000000..54ab90482a91 --- /dev/null +++ b/test/extensions/access_loggers/wasm/BUILD @@ -0,0 +1,33 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/access_loggers/wasm/test_data:test_cpp.wasm", + ]), + extension_name = "envoy.access_loggers.wasm", + deps = [ + "//source/extensions/access_loggers/wasm:config", + "//test/extensions/access_loggers/wasm/test_data:test_cpp_plugin", + "//test/mocks/server:server_mocks", + "//test/test_common:environment_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/access_loggers/wasm/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/access_loggers/wasm/config_test.cc b/test/extensions/access_loggers/wasm/config_test.cc new file mode 100644 index 000000000000..02a71c9132b7 --- /dev/null +++ b/test/extensions/access_loggers/wasm/config_test.cc @@ -0,0 +1,118 @@ +#include "envoy/extensions/access_loggers/wasm/v3/wasm.pb.h" +#include "envoy/registry/registry.h" + +#include "common/access_log/access_log_impl.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/access_loggers/wasm/config.h" +#include "extensions/access_loggers/wasm/wasm_access_log_impl.h" +#include "extensions/access_loggers/well_known_names.h" +#include "extensions/common/wasm/wasm.h" + +#include "test/mocks/server/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace AccessLoggers { +namespace Wasm { + +class TestFactoryContext : public NiceMock { +public: + TestFactoryContext(Api::Api& api, Stats::Scope& scope) : api_(api), scope_(scope) {} + Api::Api& api() override { return api_; } + Stats::Scope& scope() override { return scope_; } + const envoy::config::core::v3::Metadata& listenerMetadata() const override { + return listener_metadata_; + } + +private: + Api::Api& api_; + Stats::Scope& scope_; + envoy::config::core::v3::Metadata listener_metadata_; +}; + +class WasmAccessLogConfigTest : public testing::TestWithParam {}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmAccessLogConfigTest, testing_values); + +TEST_P(WasmAccessLogConfigTest, CreateWasmFromEmpty) { + auto factory = + Registry::FactoryRegistry::getFactory( + AccessLogNames::get().Wasm); + ASSERT_NE(factory, nullptr); + + ProtobufTypes::MessagePtr message = factory->createEmptyConfigProto(); + ASSERT_NE(nullptr, message); + + AccessLog::FilterPtr filter; + NiceMock context; + + AccessLog::InstanceSharedPtr instance; + EXPECT_THROW_WITH_MESSAGE( + instance = factory->createAccessLogInstance(*message, std::move(filter), context), + Common::Wasm::WasmException, "Unable to create Wasm access log "); +} + +TEST_P(WasmAccessLogConfigTest, CreateWasmFromWASM) { + auto factory = + Registry::FactoryRegistry::getFactory( + AccessLogNames::get().Wasm); + ASSERT_NE(factory, nullptr); + + envoy::extensions::access_loggers::wasm::v3::WasmAccessLog config; + config.mutable_config()->mutable_vm_config()->set_runtime( + absl::StrCat("envoy.wasm.runtime.", GetParam())); + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/access_loggers/wasm/test_data/test_cpp.wasm")); + } else { + code = "AccessLoggerTestCpp"; + } + config.mutable_config()->mutable_vm_config()->mutable_code()->mutable_local()->set_inline_bytes( + code); + // Test Any configuration. + ProtobufWkt::Struct some_proto; + config.mutable_config()->mutable_vm_config()->mutable_configuration()->PackFrom(some_proto); + + AccessLog::FilterPtr filter; + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + TestFactoryContext context(*api, stats_store); + + AccessLog::InstanceSharedPtr instance = + factory->createAccessLogInstance(config, std::move(filter), context); + EXPECT_NE(nullptr, instance); + EXPECT_NE(nullptr, dynamic_cast(instance.get())); + Http::TestRequestHeaderMapImpl request_header; + Http::TestResponseHeaderMapImpl response_header; + Http::TestResponseTrailerMapImpl response_trailer; + StreamInfo::MockStreamInfo log_stream_info; + instance->log(&request_header, &response_header, &response_trailer, log_stream_info); + + filter = std::make_unique>(); + AccessLog::InstanceSharedPtr filter_instance = + factory->createAccessLogInstance(config, std::move(filter), context); + filter_instance->log(&request_header, &response_header, &response_trailer, log_stream_info); +} + +} // namespace Wasm +} // namespace AccessLoggers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/access_loggers/wasm/test_data/BUILD b/test/extensions/access_loggers/wasm/test_data/BUILD new file mode 100644 index 000000000000..f49006867f2f --- /dev/null +++ b/test/extensions/access_loggers/wasm/test_data/BUILD @@ -0,0 +1,35 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary") + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "test_cpp_plugin", + srcs = [ + "test_cpp.cc", + "test_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_wasm_cc_binary( + name = "test_cpp.wasm", + srcs = ["test_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics_lite", + ], +) diff --git a/test/extensions/access_loggers/wasm/test_data/test_cpp.cc b/test/extensions/access_loggers/wasm/test_data/test_cpp.cc new file mode 100644 index 000000000000..18e59d57ddfc --- /dev/null +++ b/test/extensions/access_loggers/wasm/test_data/test_cpp.cc @@ -0,0 +1,26 @@ +// NOLINT(namespace-envoy) +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#else +#include "include/proxy-wasm/null_plugin.h" +#endif + +START_WASM_PLUGIN(AccessLoggerTestCpp) + +class TestRootContext : public RootContext { +public: + using RootContext::RootContext; + + void onLog() override; +}; +static RegisterContextFactory register_ExampleContext(ROOT_FACTORY(TestRootContext)); + +void TestRootContext::onLog() { + auto path = getRequestHeader(":path"); + logWarn("onLog " + std::to_string(id()) + " " + std::string(path->view())); +} + +END_WASM_PLUGIN diff --git a/test/extensions/access_loggers/wasm/test_data/test_cpp_null_plugin.cc b/test/extensions/access_loggers/wasm/test_data/test_cpp_null_plugin.cc new file mode 100644 index 000000000000..2fcfdddcbab2 --- /dev/null +++ b/test/extensions/access_loggers/wasm/test_data/test_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace AccessLoggerTestCpp { +NullPluginRegistry* context_registry_; +} // namespace AccessLoggerTestCpp + +RegisterNullVmPluginFactory register_common_wasm_test_cpp_plugin("AccessLoggerTestCpp", []() { + return std::make_unique(AccessLoggerTestCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/bootstrap/wasm/BUILD b/test/extensions/bootstrap/wasm/BUILD new file mode 100644 index 000000000000..6a6488e2b63b --- /dev/null +++ b/test/extensions/bootstrap/wasm/BUILD @@ -0,0 +1,93 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", + "envoy_extension_cc_test_binary", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "wasm_test", + srcs = ["wasm_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/bootstrap/wasm/test_data:asm2wasm_cpp.wasm", + "//test/extensions/bootstrap/wasm/test_data:bad_signature_cpp.wasm", + "//test/extensions/bootstrap/wasm/test_data:emscripten_cpp.wasm", + "//test/extensions/bootstrap/wasm/test_data:logging_cpp.wasm", + "//test/extensions/bootstrap/wasm/test_data:logging_rust.wasm", + "//test/extensions/bootstrap/wasm/test_data:segv_cpp.wasm", + "//test/extensions/bootstrap/wasm/test_data:stats_cpp.wasm", + ]), + extension_name = "envoy.bootstrap.wasm", + external_deps = ["abseil_optional"], + deps = [ + "//source/common/event:dispatcher_lib", + "//source/common/stats:isolated_store_lib", + "//source/common/stats:stats_lib", + "//source/extensions/bootstrap/wasm:config", + "//source/extensions/common/wasm:wasm_lib", + "//test/extensions/bootstrap/wasm/test_data:stats_cpp_plugin", + "//test/mocks/server:server_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/bootstrap/wasm/test_data:missing_cpp.wasm", + "//test/extensions/bootstrap/wasm/test_data:start_cpp.wasm", + ]), + extension_name = "envoy.bootstrap.wasm", + deps = [ + "//include/envoy/registry", + "//source/common/stats:isolated_store_lib", + "//source/extensions/bootstrap/wasm:config", + "//source/extensions/common/wasm:wasm_lib", + "//test/extensions/bootstrap/wasm/test_data:start_cpp_plugin", + "//test/mocks/event:event_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", + "@envoy_api//envoy/extensions/wasm/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test_binary( + name = "wasm_speed_test", + srcs = ["wasm_speed_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/bootstrap/wasm/test_data:speed_cpp.wasm", + ]), + extension_name = "envoy.bootstrap.wasm", + external_deps = [ + "abseil_optional", + "benchmark", + ], + deps = [ + "//source/common/event:dispatcher_lib", + "//source/common/stats:isolated_store_lib", + "//source/common/stats:stats_lib", + "//source/extensions/bootstrap/wasm:config", + "//source/extensions/common/wasm:wasm_lib", + "//test/extensions/bootstrap/wasm/test_data:speed_cpp_plugin", + "//test/mocks/server:server_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + ], +) diff --git a/test/extensions/bootstrap/wasm/config_test.cc b/test/extensions/bootstrap/wasm/config_test.cc new file mode 100644 index 000000000000..6fb99261a3f8 --- /dev/null +++ b/test/extensions/bootstrap/wasm/config_test.cc @@ -0,0 +1,158 @@ +#include "envoy/common/exception.h" +#include "envoy/extensions/wasm/v3/wasm.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/stats/isolated_store_impl.h" + +#include "extensions/bootstrap/wasm/config.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Wasm { + +using Extensions::Bootstrap::Wasm::WasmServicePtr; + +class WasmFactoryTest : public testing::TestWithParam { +protected: + WasmFactoryTest() { + config_.mutable_config()->mutable_vm_config()->set_runtime( + absl::StrCat("envoy.wasm.runtime.", GetParam())); + if (GetParam() != "null") { + config_.mutable_config()->mutable_vm_config()->mutable_code()->mutable_local()->set_filename( + TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/start_cpp.wasm")); + } else { + config_.mutable_config() + ->mutable_vm_config() + ->mutable_code() + ->mutable_local() + ->set_inline_bytes("WasmStartCpp"); + } + config_.mutable_config()->set_name("test"); + config_.set_singleton(true); + } + + void initializeWithConfig(const envoy::extensions::wasm::v3::WasmService& config) { + auto factory = + Registry::FactoryRegistry::getFactory( + "envoy.bootstrap.wasm"); + ASSERT_NE(factory, nullptr); + api_ = Api::createApiForTest(stats_store_); + EXPECT_CALL(context_, api()).WillRepeatedly(testing::ReturnRef(*api_)); + EXPECT_CALL(context_, initManager()).WillRepeatedly(testing::ReturnRef(init_manager_)); + EXPECT_CALL(context_, lifecycleNotifier()) + .WillRepeatedly(testing::ReturnRef(lifecycle_notifier_)); + extension_ = factory->createBootstrapExtension(config, context_); + static_cast(extension_.get())->wasmService(); + EXPECT_CALL(init_watcher_, ready()); + init_manager_.initialize(init_watcher_); + } + + envoy::extensions::wasm::v3::WasmService config_; + testing::NiceMock context_; + testing::NiceMock lifecycle_notifier_; + Init::ExpectableWatcherImpl init_watcher_; + Stats::IsolatedStoreImpl stats_store_; + Api::ApiPtr api_; + Init::ManagerImpl init_manager_{"init_manager"}; + Server::BootstrapExtensionPtr extension_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmFactoryTest, testing_values); + +TEST_P(WasmFactoryTest, CreateWasmFromWasm) { + auto factory = std::make_unique(); + auto empty_config = factory->createEmptyConfigProto(); + + initializeWithConfig(config_); + + EXPECT_NE(extension_, nullptr); +} + +TEST_P(WasmFactoryTest, CreateWasmFromWasmPerThread) { + config_.set_singleton(false); + initializeWithConfig(config_); + + EXPECT_NE(extension_, nullptr); + extension_.reset(); + context_.threadLocal().shutdownThread(); +} + +TEST_P(WasmFactoryTest, MissingImport) { + if (GetParam() == "null") { + return; + } + config_.mutable_config()->mutable_vm_config()->mutable_code()->mutable_local()->set_filename( + TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/missing_cpp.wasm")); + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config_), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm service test"); +} + +TEST_P(WasmFactoryTest, UnspecifiedRuntime) { + config_.mutable_config()->mutable_vm_config()->set_runtime(""); + + EXPECT_THROW_WITH_REGEX( + initializeWithConfig(config_), EnvoyException, + "Proto constraint validation failed \\(WasmServiceValidationError\\.Config"); +} + +TEST_P(WasmFactoryTest, UnknownRuntime) { + config_.mutable_config()->mutable_vm_config()->set_runtime("envoy.wasm.runtime.invalid"); + + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config_), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm service test"); +} + +TEST_P(WasmFactoryTest, StartFailed) { + ProtobufWkt::StringValue plugin_configuration; + plugin_configuration.set_value("bad"); + config_.mutable_config()->mutable_vm_config()->mutable_configuration()->PackFrom( + plugin_configuration); + + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config_), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm service test"); +} + +TEST_P(WasmFactoryTest, StartFailedOpen) { + ProtobufWkt::StringValue plugin_configuration; + plugin_configuration.set_value("bad"); + config_.mutable_config()->mutable_vm_config()->mutable_configuration()->PackFrom( + plugin_configuration); + config_.mutable_config()->set_fail_open(true); + + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config_), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm service test"); +} + +TEST_P(WasmFactoryTest, ConfigureFailed) { + ProtobufWkt::StringValue plugin_configuration; + plugin_configuration.set_value("bad"); + config_.mutable_config()->mutable_configuration()->PackFrom(plugin_configuration); + + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config_), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm service test"); +} + +} // namespace Wasm +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/wasm/test_data/BUILD b/test/extensions/bootstrap/wasm/test_data/BUILD new file mode 100644 index 000000000000..cce684b45755 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/BUILD @@ -0,0 +1,146 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary", "wasm_rust_binary") + +licenses(["notice"]) # Apache 2 + +envoy_package() + +wasm_rust_binary( + name = "logging_rust.wasm", + srcs = ["logging_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +envoy_cc_library( + name = "speed_cpp_plugin", + srcs = [ + "speed_cpp.cc", + "speed_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "start_cpp_plugin", + srcs = [ + "start_cpp.cc", + "start_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_cc_library( + name = "stats_cpp_plugin", + srcs = [ + "stats_cpp.cc", + "stats_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_wasm_cc_binary( + name = "asm2wasm_cpp.wasm", + srcs = ["asm2wasm_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "bad_signature_cpp.wasm", + srcs = ["bad_signature_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "emscripten_cpp.wasm", + srcs = ["emscripten_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "logging_cpp.wasm", + srcs = ["logging_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "missing_cpp.wasm", + srcs = ["missing_cpp.cc"], + linkopts = [ + "--js-library external/proxy_wasm_cpp_sdk/proxy_wasm_intrinsics.js", + "-s ERROR_ON_UNDEFINED_SYMBOLS=0", + ], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "segv_cpp.wasm", + srcs = ["segv_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "speed_cpp.wasm", + srcs = ["speed_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics_full", + ], +) + +envoy_wasm_cc_binary( + name = "start_cpp.wasm", + srcs = ["start_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "stats_cpp.wasm", + srcs = ["stats_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) diff --git a/test/extensions/bootstrap/wasm/test_data/asm2wasm_cpp.cc b/test/extensions/bootstrap/wasm/test_data/asm2wasm_cpp.cc new file mode 100644 index 000000000000..abb7b89fa705 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/asm2wasm_cpp.cc @@ -0,0 +1,23 @@ +// NOLINT(namespace-envoy) +#include + +#include + +#include "proxy_wasm_intrinsics.h" + +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} + +// Use global variables so the compiler cannot optimize the operations away. +int32_t i32a = 0; +int32_t i32b = 1; +double f64a = 0.0; +double f64b = 1.0; + +// Emscripten in some modes and versions would use functions from the `asm2wasm` module to implement +// these operations: int32_t % /, double conversion to int32_t and remainder(). +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_configure(uint32_t, uint32_t) { + logInfo(std::string("out ") + std::to_string(i32a / i32b) + " " + std::to_string(i32a % i32b) + + " " + std::to_string((int32_t)remainder(f64a, f64b))); + return 1; +} diff --git a/test/extensions/bootstrap/wasm/test_data/bad_signature_cpp.cc b/test/extensions/bootstrap/wasm/test_data/bad_signature_cpp.cc new file mode 100644 index 000000000000..29150365c2e1 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/bad_signature_cpp.cc @@ -0,0 +1,16 @@ +// NOLINT(namespace-envoy) +#include + +#define PROXY_WASM_KEEPALIVE __attribute__((used)) __attribute__((visibility("default"))) + +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} + +extern "C" uint32_t proxy_log(uint32_t level, const char* logMessage, size_t messageSize); + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_configure(uint32_t, int bad, char* configuration, + int size) { + std::string message = "bad signature"; + proxy_log(4 /* error */, message.c_str(), message.size()); + return 1; +} diff --git a/test/extensions/bootstrap/wasm/test_data/emscripten_cpp.cc b/test/extensions/bootstrap/wasm/test_data/emscripten_cpp.cc new file mode 100644 index 000000000000..106c1df8cd57 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/emscripten_cpp.cc @@ -0,0 +1,20 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#include "proxy_wasm_intrinsics.h" + +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} + +float gNan = std::nan("1"); +float gInfinity = INFINITY; + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_configure(uint32_t, uint32_t) { + logInfo(std::string("NaN ") + std::to_string(gNan)); + logWarn("inf " + std::to_string(gInfinity)); + logWarn("inf " + std::to_string(1.0 / 0.0)); + logWarn(std::string("inf ") + (std::isinf(gInfinity) ? "inf" : "nan")); + return 1; +} diff --git a/test/extensions/bootstrap/wasm/test_data/logging_cpp.cc b/test/extensions/bootstrap/wasm/test_data/logging_cpp.cc new file mode 100644 index 000000000000..70fde8f6ae19 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/logging_cpp.cc @@ -0,0 +1,47 @@ +// NOLINT(namespace-envoy) +#include + +#include + +#include "proxy_wasm_intrinsics.h" + +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_configure(uint32_t, uint32_t configuration_size) { + fprintf(stdout, "printf stdout test"); + fflush(stdout); + fprintf(stderr, "printf stderr test"); + logTrace("test trace logging"); + logDebug("test debug logging"); + logError("test error logging"); + const char* configuration = nullptr; + size_t size; + proxy_get_buffer_bytes(WasmBufferType::PluginConfiguration, 0, configuration_size, &configuration, + &size); + logWarn(std::string("warn " + std::string(configuration, size))); + ::free((void*)configuration); + return 1; +} + +extern "C" PROXY_WASM_KEEPALIVE void proxy_on_context_create(uint32_t, uint32_t) {} + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_vm_start(uint32_t, uint32_t) { + proxy_set_tick_period_milliseconds(10); + return 1; +} + +extern "C" PROXY_WASM_KEEPALIVE void proxy_on_tick(uint32_t) { + const char* root_id = nullptr; + size_t size; + proxy_get_property("plugin_root_id", sizeof("plugin_root_id") - 1, &root_id, &size); + logInfo("test tick logging" + std::string(root_id, size)); + proxy_done(); +} + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_done(uint32_t) { + logInfo("onDone logging"); + return 0; +} + +extern "C" PROXY_WASM_KEEPALIVE void proxy_on_delete(uint32_t) { logInfo("onDelete logging"); } diff --git a/test/extensions/bootstrap/wasm/test_data/logging_rust/Cargo.toml b/test/extensions/bootstrap/wasm/test_data/logging_rust/Cargo.toml new file mode 100644 index 000000000000..a82aed3df58d --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/logging_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm logging test" +name = "logging_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/bootstrap/wasm/test_data/logging_rust/src/lib.rs b/test/extensions/bootstrap/wasm/test_data/logging_rust/src/lib.rs new file mode 100644 index 000000000000..49947fd975c3 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/logging_rust/src/lib.rs @@ -0,0 +1,49 @@ +use log::{debug, error, info, trace, warn}; +use proxy_wasm::traits::{Context, RootContext}; +use proxy_wasm::types::LogLevel; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(TestRoot) }); +} + +struct TestRoot; + +impl RootContext for TestRoot { + fn on_vm_start(&mut self, _: usize) -> bool { + true + } + + fn on_configure(&mut self, _: usize) -> bool { + trace!("test trace logging"); + debug!("test debug logging"); + error!("test error logging"); + if let Some(value) = self.get_configuration() { + warn!("warn {}", String::from_utf8(value).unwrap()); + } + true + } + + fn on_tick(&mut self) { + if let Some(value) = self.get_property(vec!["plugin_root_id"]) { + info!("test tick logging{}", String::from_utf8(value).unwrap()); + } else { + info!("test tick logging"); + } + self.done(); + } +} + +impl Context for TestRoot { + fn on_done(&mut self) -> bool { + info!("onDone logging"); + false + } +} + +impl Drop for TestRoot { + fn drop(&mut self) { + info!("onDelete logging"); + } +} diff --git a/test/extensions/bootstrap/wasm/test_data/missing_cpp.cc b/test/extensions/bootstrap/wasm/test_data/missing_cpp.cc new file mode 100644 index 000000000000..365f3a240bef --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/missing_cpp.cc @@ -0,0 +1,12 @@ +// NOLINT(namespace-envoy) +#include "proxy_wasm_intrinsics.h" + +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} + +extern "C" void missing(); + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_vm_start(uint32_t, uint32_t) { + missing(); + return 1; +} diff --git a/test/extensions/bootstrap/wasm/test_data/segv_cpp.cc b/test/extensions/bootstrap/wasm/test_data/segv_cpp.cc new file mode 100644 index 000000000000..2f6f84cfabb3 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/segv_cpp.cc @@ -0,0 +1,25 @@ +// NOLINT(namespace-envoy) +#include + +#include "proxy_wasm_intrinsics.h" + +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} + +static int* badptr = nullptr; + +extern "C" PROXY_WASM_KEEPALIVE uint32_t proxy_on_configure(uint32_t, uint32_t) { + logError("before badptr"); + *badptr = 1; + logError("after badptr"); + return 1; +} + +extern "C" PROXY_WASM_KEEPALIVE void proxy_on_log(uint32_t context_id) { + logError("before div by zero"); +#pragma clang optimize off + int zero = context_id / 1000; + logError("divide by zero: " + std::to_string(100 / zero)); +#pragma clang optimize on + logError("after div by zero"); +} diff --git a/test/extensions/bootstrap/wasm/test_data/speed_cpp.cc b/test/extensions/bootstrap/wasm/test_data/speed_cpp.cc new file mode 100644 index 000000000000..f5b3782acbde --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/speed_cpp.cc @@ -0,0 +1,345 @@ +// NOLINT(namespace-envoy) +#include + +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_full.h" +// Required Proxy-Wasm ABI version. +extern "C" PROXY_WASM_KEEPALIVE void proxy_abi_version_0_1_0() {} +#else +#include "envoy/config/core/v3/grpc_service.pb.h" +using envoy::config::core::v3::GrpcService; +#include "include/proxy-wasm/null_plugin.h" +#endif + +START_WASM_PLUGIN(WasmSpeedCpp) + +int xDoNotRemove = 0; + +google::protobuf::Arena arena; + +google::protobuf::Struct args; +google::protobuf::Struct* args_arena = + google::protobuf::Arena::CreateMessage(&arena); +std::string configuration = R"EOF( + { + "NAME":"test_pod", + "NAMESPACE":"test_namespace", + "LABELS": { + "app": "productpage", + "version": "v1", + "pod-template-hash": "84975bc778" + }, + "OWNER":"test_owner", + "WORKLOAD_NAME":"test_workload", + "PLATFORM_METADATA":{ + "gcp_project":"test_project", + "gcp_cluster_location":"test_location", + "gcp_cluster_name":"test_cluster" + }, + "ISTIO_VERSION":"istio-1.4", + "MESH_ID":"test-mesh" + } + )EOF"; + +// google::protobuf::Struct a; +// google::protobuf::util::JsonStringToMessage(configuration+'hfdjfhkjhdskhjk', a); + +const static char encodeLookup[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const static char padCharacter = '='; + +std::string base64Encode(const uint8_t* start, const uint8_t* end) { + std::string encodedString; + size_t size = end - start; + encodedString.reserve(((size / 3) + (size % 3 > 0)) * 4); + uint32_t temp; + auto cursor = start; + for (size_t idx = 0; idx < size / 3; idx++) { + temp = (*cursor++) << 16; // Convert to big endian + temp += (*cursor++) << 8; + temp += (*cursor++); + encodedString.append(1, encodeLookup[(temp & 0x00FC0000) >> 18]); + encodedString.append(1, encodeLookup[(temp & 0x0003F000) >> 12]); + encodedString.append(1, encodeLookup[(temp & 0x00000FC0) >> 6]); + encodedString.append(1, encodeLookup[(temp & 0x0000003F)]); + } + switch (size % 3) { + case 1: + temp = (*cursor++) << 16; // Convert to big endian + encodedString.append(1, encodeLookup[(temp & 0x00FC0000) >> 18]); + encodedString.append(1, encodeLookup[(temp & 0x0003F000) >> 12]); + encodedString.append(2, padCharacter); + break; + case 2: + temp = (*cursor++) << 16; // Convert to big endian + temp += (*cursor++) << 8; + encodedString.append(1, encodeLookup[(temp & 0x00FC0000) >> 18]); + encodedString.append(1, encodeLookup[(temp & 0x0003F000) >> 12]); + encodedString.append(1, encodeLookup[(temp & 0x00000FC0) >> 6]); + encodedString.append(1, padCharacter); + break; + } + return encodedString; +} + +bool base64Decode(const std::basic_string& input, std::vector* output) { + if (input.length() % 4) + return false; + size_t padding = 0; + if (input.length()) { + if (input[input.length() - 1] == padCharacter) + padding++; + if (input[input.length() - 2] == padCharacter) + padding++; + } + // Setup a vector to hold the result + std::vector decodedBytes; + decodedBytes.reserve(((input.length() / 4) * 3) - padding); + uint32_t temp = 0; // Holds decoded quanta + std::basic_string::const_iterator cursor = input.begin(); + while (cursor < input.end()) { + for (size_t quantumPosition = 0; quantumPosition < 4; quantumPosition++) { + temp <<= 6; + if (*cursor >= 0x41 && *cursor <= 0x5A) // This area will need tweaking if + temp |= *cursor - 0x41; // you are using an alternate alphabet + else if (*cursor >= 0x61 && *cursor <= 0x7A) + temp |= *cursor - 0x47; + else if (*cursor >= 0x30 && *cursor <= 0x39) + temp |= *cursor + 0x04; + else if (*cursor == 0x2B) + temp |= 0x3E; // change to 0x2D for URL alphabet + else if (*cursor == 0x2F) + temp |= 0x3F; // change to 0x5F for URL alphabet + else if (*cursor == padCharacter) { // pad + switch (input.end() - cursor) { + case 1: // One pad character + decodedBytes.push_back((temp >> 16) & 0x000000FF); + decodedBytes.push_back((temp >> 8) & 0x000000FF); + goto Ldone; + case 2: // Two pad characters + decodedBytes.push_back((temp >> 10) & 0x000000FF); + goto Ldone; + default: + return false; + } + } else + return false; + cursor++; + } + decodedBytes.push_back((temp >> 16) & 0x000000FF); + decodedBytes.push_back((temp >> 8) & 0x000000FF); + decodedBytes.push_back((temp)&0x000000FF); + } +Ldone: + *output = std::move(decodedBytes); + return true; +} +std::string check_compiler; + +void (*test_fn)() = nullptr; + +void empty_test() {} + +void get_current_time_test() { + uint64_t t; + if (WasmResult::Ok != proxy_get_current_time_nanoseconds(&t)) { + logError("bad result from getCurrentTimeNanoseconds"); + } +} + +void small_string_check_compiler_test() { + check_compiler = "foo"; + check_compiler += "bar"; + check_compiler = ""; +} + +void small_string_test() { + std::string s = "foo"; + s += "bar"; + xDoNotRemove = s.size(); +} + +void small_string_check_compiler1000_test() { + for (int x = 0; x < 1000; x++) { + check_compiler = "foo"; + check_compiler += "bar"; + } + check_compiler = ""; +} + +void small_string1000_test() { + for (int x = 0; x < 1000; x++) { + std::string s = "foo"; + s += "bar"; + xDoNotRemove += s.size(); + } +} + +void large_string_test() { + std::string s(1024, 'f'); + std::string d(1024, 'o'); + s += d; + xDoNotRemove += s.size(); +} + +void large_string1000_test() { + for (int x = 0; x < 1000; x++) { + std::string s(1024, 'f'); + std::string d(1024, 'o'); + s += d; + xDoNotRemove += s.size(); + } +} + +void get_property_test() { + std::string property = "plugin_root_id"; + const char* value_ptr = nullptr; + size_t value_size = 0; + auto result = proxy_get_property(property.data(), property.size(), &value_ptr, &value_size); + if (WasmResult::Ok != result) { + logError("bad result for getProperty"); + } + ::free(reinterpret_cast(const_cast(value_ptr))); +} + +void grpc_service_test() { + std::string value = "foo"; + GrpcService grpc_service; + grpc_service.mutable_envoy_grpc()->set_cluster_name(value); + std::string grpc_service_string; + grpc_service.SerializeToString(&grpc_service_string); +} + +void grpc_service1000_test() { + std::string value = "foo"; + for (int x = 0; x < 1000; x++) { + GrpcService grpc_service; + grpc_service.mutable_envoy_grpc()->set_cluster_name(value); + std::string grpc_service_string; + grpc_service.SerializeToString(&grpc_service_string); + } +} + +void modify_metadata_test() { + auto path = getRequestHeader(":path"); + addRequestHeader("newheader", "newheadervalue"); + auto server = getRequestHeader("server"); + replaceRequestHeader("server", "envoy-wasm"); + replaceRequestHeader("envoy-wasm", "server"); + removeRequestHeader("newheader"); +} + +void modify_metadata1000_test() { + for (int x = 0; x < 1000; x++) { + auto path = getRequestHeader(":path"); + addRequestHeader("newheader", "newheadervalue"); + auto server = getRequestHeader("server"); + replaceRequestHeader("server", "envoy-wasm"); + replaceRequestHeader("envoy-wasm", "server"); + removeRequestHeader("newheader"); + } +} + +void json_serialize_test() { google::protobuf::util::JsonStringToMessage(configuration, &args); } + +void json_serialize_arena_test() { + google::protobuf::util::JsonStringToMessage(configuration, args_arena); +} + +void json_deserialize_test() { + std::string json; + google::protobuf::util::MessageToJsonString(args, &json); + xDoNotRemove += json.size(); +} + +void json_deserialize_arena_test() { + std::string json; + google::protobuf::util::MessageToJsonString(*args_arena, &json); +} + +void json_deserialize_empty_test() { + std::string json; + google::protobuf::Struct empty; + google::protobuf::util::MessageToJsonString(empty, &json); + xDoNotRemove = json.size(); +} + +void json_serialize_deserialize_test() { + std::string json; + google::protobuf::Struct proto; + google::protobuf::util::JsonStringToMessage(configuration, &proto); + google::protobuf::util::MessageToJsonString(proto, &json); + xDoNotRemove = json.size(); +} + +void convert_to_filter_state_test() { + auto start = reinterpret_cast(&*configuration.begin()); + auto end = start + configuration.size(); + std::string encoded_config = base64Encode(start, end); + std::vector decoded; + base64Decode(encoded_config, &decoded); + std::string decoded_config(decoded.begin(), decoded.end()); + google::protobuf::util::JsonStringToMessage(decoded_config, &args); + auto bytes = args.SerializeAsString(); + setFilterStateStringValue("wasm_request_set_key", bytes); +} + +WASM_EXPORT(uint32_t, proxy_on_vm_start, (uint32_t, uint32_t configuration_size)) { + const char* configuration_ptr = nullptr; + size_t size; + proxy_get_buffer_bytes(WasmBufferType::VmConfiguration, 0, configuration_size, &configuration_ptr, + &size); + std::string configuration(configuration_ptr, size); + if (configuration == "empty") { + test_fn = &empty_test; + } else if (configuration == "get_current_time") { + test_fn = &get_current_time_test; + } else if (configuration == "small_string") { + test_fn = &small_string_test; + } else if (configuration == "small_string1000") { + test_fn = &small_string1000_test; + } else if (configuration == "small_string_check_compiler") { + test_fn = &small_string_check_compiler_test; + } else if (configuration == "small_string_check_compiler1000") { + test_fn = &small_string_check_compiler1000_test; + } else if (configuration == "large_string") { + test_fn = &large_string_test; + } else if (configuration == "large_string1000") { + test_fn = &large_string1000_test; + } else if (configuration == "get_property") { + test_fn = &get_property_test; + } else if (configuration == "grpc_service") { + test_fn = &grpc_service_test; + } else if (configuration == "grpc_service1000") { + test_fn = &grpc_service1000_test; + } else if (configuration == "modify_metadata") { + test_fn = &modify_metadata_test; + } else if (configuration == "modify_metadata1000") { + test_fn = &modify_metadata1000_test; + } else if (configuration == "json_serialize") { + test_fn = &json_serialize_test; + } else if (configuration == "json_serialize_arena") { + test_fn = &json_serialize_arena_test; + } else if (configuration == "json_deserialize") { + test_fn = &json_deserialize_test; + } else if (configuration == "json_deserialize_empty") { + test_fn = &json_deserialize_empty_test; + } else if (configuration == "json_deserialize_arena") { + test_fn = &json_deserialize_arena_test; + } else if (configuration == "json_serialize_deserialize") { + test_fn = &json_serialize_deserialize_test; + } else if (configuration == "convert_to_filter_state") { + test_fn = &convert_to_filter_state_test; + } else { + std::string message = "on_start " + configuration; + proxy_log(LogLevel::info, message.c_str(), message.size()); + } + ::free(const_cast(reinterpret_cast(configuration_ptr))); + return 1; +} + +WASM_EXPORT(void, proxy_on_tick, (uint32_t)) { (*test_fn)(); } + +END_WASM_PLUGIN diff --git a/test/extensions/bootstrap/wasm/test_data/speed_cpp_null_plugin.cc b/test/extensions/bootstrap/wasm/test_data/speed_cpp_null_plugin.cc new file mode 100644 index 000000000000..c3ca3f12dea7 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/speed_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace WasmSpeedCpp { +NullPluginRegistry* context_registry_; +} // namespace WasmSpeedCpp + +RegisterNullVmPluginFactory register_wasm_speed_test_plugin("WasmSpeedCpp", []() { + return std::make_unique(WasmSpeedCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/bootstrap/wasm/test_data/start_cpp.cc b/test/extensions/bootstrap/wasm/test_data/start_cpp.cc new file mode 100644 index 000000000000..126cc1649aaa --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/start_cpp.cc @@ -0,0 +1,25 @@ +// NOLINT(namespace-envoy) +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#else +#include "include/proxy-wasm/null_plugin.h" +#endif + +START_WASM_PLUGIN(WasmStartCpp) + +// Required Proxy-Wasm ABI version. +WASM_EXPORT(void, proxy_abi_version_0_1_0, ()) {} + +WASM_EXPORT(uint32_t, proxy_on_vm_start, (uint32_t, uint32_t configuration_size)) { + logDebug("onStart"); + return configuration_size ? 0 /* failure */ : 1 /* success */; +} + +WASM_EXPORT(uint32_t, proxy_on_configure, (uint32_t, uint32_t configuration_size)) { + // Fail if we are provided a non-empty configuration. + return configuration_size ? 0 /* failure */ : 1 /* success */; +} + +END_WASM_PLUGIN diff --git a/test/extensions/bootstrap/wasm/test_data/start_cpp_null_plugin.cc b/test/extensions/bootstrap/wasm/test_data/start_cpp_null_plugin.cc new file mode 100644 index 000000000000..1d3c6ff4640a --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/start_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace WasmStartCpp { +NullPluginRegistry* context_registry_; +} // namespace WasmStartCpp + +RegisterNullVmPluginFactory register_wasm_speed_test_plugin("WasmStartCpp", []() { + return std::make_unique(WasmStartCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/bootstrap/wasm/test_data/stats_cpp.cc b/test/extensions/bootstrap/wasm/test_data/stats_cpp.cc new file mode 100644 index 000000000000..f36f85c685af --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/stats_cpp.cc @@ -0,0 +1,113 @@ +// NOLINT(namespace-envoy) +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#else +#include "include/proxy-wasm/null_plugin.h" +#endif + +template std::unique_ptr wrap_unique(T* ptr) { return std::unique_ptr(ptr); } + +START_WASM_PLUGIN(WasmStatsCpp) + +// Required Proxy-Wasm ABI version. +WASM_EXPORT(void, proxy_abi_version_0_1_0, ()) {} + +// Test the low level interface. +WASM_EXPORT(uint32_t, proxy_on_configure, (uint32_t, uint32_t)) { + uint32_t c, g, h; + CHECK_RESULT(defineMetric(MetricType::Counter, "test_counter", &c)); + CHECK_RESULT(defineMetric(MetricType::Gauge, "test_gauge", &g)); + CHECK_RESULT(defineMetric(MetricType::Histogram, "test_histogram", &h)); + + CHECK_RESULT(incrementMetric(c, 1)); + CHECK_RESULT(recordMetric(g, 2)); + CHECK_RESULT(recordMetric(h, 3)); + + uint64_t value; + CHECK_RESULT(getMetric(c, &value)); + logTrace(std::string("get counter = ") + std::to_string(value)); + CHECK_RESULT(incrementMetric(c, 1)); + CHECK_RESULT(getMetric(c, &value)); + logDebug(std::string("get counter = ") + std::to_string(value)); + CHECK_RESULT(recordMetric(c, 3)); + CHECK_RESULT(getMetric(c, &value)); + logInfo(std::string("get counter = ") + std::to_string(value)); + CHECK_RESULT(getMetric(g, &value)); + logWarn(std::string("get gauge = ") + std::to_string(value)); + // Get on histograms is not supported. + if (getMetric(h, &value) != WasmResult::Ok) { + logError(std::string("get histogram = Unsupported")); + } + return 1; +} + +// Test the higher level interface. +WASM_EXPORT(void, proxy_on_tick, (uint32_t)) { + Metric c(MetricType::Counter, "test_counter", + {MetricTag{"counter_tag", MetricTag::TagType::String}}); + Metric g(MetricType::Gauge, "test_gauge", {MetricTag{"gauge_int_tag", MetricTag::TagType::Int}}); + Metric h(MetricType::Histogram, "test_histogram", + {MetricTag{"histogram_int_tag", MetricTag::TagType::Int}, + MetricTag{"histogram_string_tag", MetricTag::TagType::String}, + MetricTag{"histogram_bool_tag", MetricTag::TagType::Bool}}); + + c.increment(1, "test_tag"); + g.record(2, 9); + h.record(3, 7, "test_tag", true); + + logTrace(std::string("get counter = ") + std::to_string(c.get("test_tag"))); + c.increment(1, "test_tag"); + logDebug(std::string("get counter = ") + std::to_string(c.get("test_tag"))); + c.record(3, "test_tag"); + logInfo(std::string("get counter = ") + std::to_string(c.get("test_tag"))); + logWarn(std::string("get gauge = ") + std::to_string(g.get(9))); + + auto hh = h.partiallyResolve(7); + auto h_id = hh.resolve("test_tag", true); + logError(std::string("resolved histogram name = ") + hh.nameFromIdSlow(h_id)); +} + +// Test the high level interface. +WASM_EXPORT(void, proxy_on_log, (uint32_t /* context_zero */)) { + auto c = wrap_unique( + Counter::New("test_counter", "string_tag", "int_tag", "bool_tag")); + auto g = + wrap_unique(Gauge::New("test_gauge", "string_tag1", "string_tag2")); + auto h = wrap_unique(Histogram::New("test_histogram", "int_tag", + "string_tag", "bool_tag")); + + c->increment(1, "test_tag", 7, true); + logTrace(std::string("get counter = ") + std::to_string(c->get("test_tag", 7, true))); + auto simple_c = c->resolve("test_tag", 7, true); + simple_c++; + logDebug(std::string("get counter = ") + std::to_string(c->get("test_tag", 7, true))); + c->record(3, "test_tag", 7, true); + logInfo(std::string("get counter = ") + std::to_string(c->get("test_tag", 7, true))); + + g->record(2, "test_tag1", "test_tag2"); + logWarn(std::string("get gauge = ") + std::to_string(g->get("test_tag1", "test_tag2"))); + + h->record(3, 7, "test_tag", true); + auto base_h = wrap_unique(Counter::New("test_histogram", "int_tag")); + auto complete_h = + wrap_unique(base_h->extendAndResolve(7, "string_tag", "bool_tag")); + auto simple_h = complete_h->resolve("test_tag", true); + logError(std::string("h_id = ") + complete_h->nameFromIdSlow(simple_h.metric_id)); + + Counter stack_c("test_counter", "string_tag", "int_tag", "bool_tag"); + stack_c.increment(1, "test_tag_stack", 7, true); + logError(std::string("stack_c = ") + std::to_string(stack_c.get("test_tag_stack", 7, true))); + + Gauge stack_g("test_gauge", "string_tag1", "string_tag2"); + stack_g.record(2, "stack_test_tag1", "test_tag2"); + logError(std::string("stack_g = ") + std::to_string(stack_g.get("stack_test_tag1", "test_tag2"))); + + std::string_view int_tag = "int_tag"; + Histogram stack_h("test_histogram", int_tag, "string_tag", "bool_tag"); + std::string_view stack_test_tag = "stack_test_tag"; + stack_h.record(3, 7, stack_test_tag, true); +} + +END_WASM_PLUGIN diff --git a/test/extensions/bootstrap/wasm/test_data/stats_cpp_null_plugin.cc b/test/extensions/bootstrap/wasm/test_data/stats_cpp_null_plugin.cc new file mode 100644 index 000000000000..35abc74861d5 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/stats_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace WasmStatsCpp { +NullPluginRegistry* context_registry_; +} // namespace WasmStatsCpp + +RegisterNullVmPluginFactory register_wasm_speed_test_plugin("WasmStatsCpp", []() { + return std::make_unique(WasmStatsCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/bootstrap/wasm/wasm_speed_test.cc b/test/extensions/bootstrap/wasm/wasm_speed_test.cc new file mode 100644 index 000000000000..6d39d399fb89 --- /dev/null +++ b/test/extensions/bootstrap/wasm/wasm_speed_test.cc @@ -0,0 +1,144 @@ +/** + * Simple WASM speed test. + * + * Run with: + * `bazel run --config=libc++ -c opt //test/extensions/bootstrap/wasm:wasm_speed_test` + */ +#include "common/event/dispatcher_impl.h" +#include "common/stats/isolated_store_impl.h" + +#include "extensions/common/wasm/wasm.h" + +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "absl/types/optional.h" +#include "benchmark/benchmark.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "tools/cpp/runfiles/runfiles.h" + +using bazel::tools::cpp::runfiles::Runfiles; + +namespace Envoy { +namespace Extensions { +namespace Wasm { + +class TestRoot : public Envoy::Extensions::Common::Wasm::Context { +public: + TestRoot(Extensions::Common::Wasm::Wasm* wasm, + const std::shared_ptr& plugin) + : Envoy::Extensions::Common::Wasm::Context(wasm, plugin) {} + + using Envoy::Extensions::Common::Wasm::Context::log; + proxy_wasm::WasmResult log(uint32_t level, absl::string_view message) override { + log_(static_cast(level), message); + return proxy_wasm::WasmResult::Ok; + } + MOCK_METHOD2(log_, void(spdlog::level::level_enum level, absl::string_view message)); +}; + +static void bmWasmSimpleCallSpeedTest(benchmark::State& state, std::string test, + std::string runtime) { + Envoy::Logger::Registry::getLog(Logger::Id::wasm).set_level(spdlog::level::off); + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = "some_long_root_id"; + auto vm_id = ""; + auto vm_configuration = test; + auto vm_key = ""; + auto plugin_configuration = ""; + auto plugin = std::make_shared( + name, root_id, vm_id, runtime, plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", runtime), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + std::string code; + if (runtime == "null") { + code = "WasmSpeedCpp"; + } else { + code = TestEnvironment::readFileToStringForTest( + TestEnvironment::runfilesPath("test/extensions/bootstrap/wasm/test_data/speed_cpp.wasm")); + } + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, + [](Extensions::Common::Wasm::Wasm* wasm, + const std::shared_ptr& plugin) + -> proxy_wasm::ContextBase* { return new TestRoot(wasm, plugin); }); + + auto root_context = wasm->start(plugin); + for (__attribute__((unused)) auto _ : state) { + root_context->onTick(0); + } +} + +#if defined(ENVOY_WASM_WAVM) +#define B(_t) \ + BENCHMARK_CAPTURE(bmWasmSimpleCallSpeedTest, V8SpeedTest_##_t, std::string(#_t), \ + std::string("v8")); \ + BENCHMARK_CAPTURE(bmWasmSimpleCallSpeedTest, NullSpeedTest_##_t, std::string(#_t), \ + std::string("null")); \ + BENCHMARK_CAPTURE(bmWasmSimpleCallSpeedTest, WavmSpeedTest_##_t, std::string(#_t), \ + std::string("wavm")); +#else +#define B(_t) \ + BENCHMARK_CAPTURE(bmWasmSimpleCallSpeedTest, V8SpeedTest_##_t, std::string(#_t), \ + std::string("v8")); \ + BENCHMARK_CAPTURE(bmWasmSimpleCallSpeedTest, NullSpeedTest_##_t, std::string(#_t), \ + std::string("null")); +#endif + +B(empty) +B(get_current_time) +B(small_string) +B(small_string1000) +B(small_string_check_compiler) +B(small_string_check_compiler1000) +B(large_string) +B(large_string1000) +B(get_property) +B(grpc_service) +B(grpc_service1000) +B(modify_metadata) +B(modify_metadata1000) +B(json_serialize) +B(json_serialize_arena) +B(json_deserialize) +B(json_deserialize_arena) +B(json_deserialize_empty) +B(json_serialize_deserialize) +B(convert_to_filter_state) + +} // namespace Wasm +} // namespace Extensions +} // namespace Envoy + +int main(int argc, char** argv) { + ::benchmark::Initialize(&argc, argv); + Envoy::TestEnvironment::initializeOptions(argc, argv); + // Create a Runfiles object for runfiles lookup. + // https://github.com/bazelbuild/bazel/blob/master/tools/cpp/runfiles/runfiles_src.h#L32 + std::string error; + std::unique_ptr runfiles(Runfiles::Create(argv[0], &error)); + RELEASE_ASSERT(Envoy::TestEnvironment::getOptionalEnvVar("NORUNFILES").has_value() || + runfiles != nullptr, + error); + Envoy::TestEnvironment::setRunfiles(runfiles.get()); + Envoy::TestEnvironment::setEnvVar("ENVOY_IP_TEST_VERSIONS", "all", 0); + Envoy::Event::Libevent::Global::initialize(); + if (::benchmark::ReportUnrecognizedArguments(argc, argv)) { + return 1; + } + ::benchmark::RunSpecifiedBenchmarks(); + return 0; +} diff --git a/test/extensions/bootstrap/wasm/wasm_test.cc b/test/extensions/bootstrap/wasm/wasm_test.cc new file mode 100644 index 000000000000..9511a91c96a9 --- /dev/null +++ b/test/extensions/bootstrap/wasm/wasm_test.cc @@ -0,0 +1,343 @@ +#include "common/event/dispatcher_impl.h" +#include "common/stats/isolated_store_impl.h" + +#include "extensions/common/wasm/wasm.h" + +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "absl/types/optional.h" +#include "gmock/gmock.h" +#include "gtest/gtest-param-test.h" +#include "gtest/gtest.h" + +using testing::Eq; + +namespace Envoy { +namespace Extensions { +namespace Wasm { + +class TestContext : public Extensions::Common::Wasm::Context { +public: + TestContext(Extensions::Common::Wasm::Wasm* wasm, + const std::shared_ptr& plugin) + : Extensions::Common::Wasm::Context(wasm, plugin) {} + ~TestContext() override = default; + using Extensions::Common::Wasm::Context::log; + proxy_wasm::WasmResult log(uint32_t level, absl::string_view message) override { + std::cerr << std::string(message) << "\n"; + log_(static_cast(level), message); + return proxy_wasm::WasmResult::Ok; + } + MOCK_METHOD2(log_, void(spdlog::level::level_enum level, absl::string_view message)); +}; + +class WasmTestBase { +public: + WasmTestBase() + : api_(Api::createApiForTest(stats_store_)), + dispatcher_(api_->allocateDispatcher("wasm_test")), + base_scope_(stats_store_.createScope("")), scope_(base_scope_->createScope("")) {} + + void createWasm(absl::string_view runtime) { + plugin_ = std::make_shared( + name_, root_id_, vm_id_, runtime, plugin_configuration_, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info_, nullptr); + wasm_ = std::make_shared( + absl::StrCat("envoy.wasm.runtime.", runtime), vm_id_, vm_configuration_, vm_key_, scope_, + cluster_manager, *dispatcher_); + EXPECT_NE(wasm_, nullptr); + wasm_->setCreateContextForTesting( + nullptr, + [](Extensions::Common::Wasm::Wasm* wasm, + const std::shared_ptr& plugin) + -> proxy_wasm::ContextBase* { return new TestContext(wasm, plugin); }); + } + + Stats::IsolatedStoreImpl stats_store_; + Api::ApiPtr api_; + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher_; + Stats::ScopeSharedPtr base_scope_; + Stats::ScopeSharedPtr scope_; + NiceMock local_info_; + std::string name_; + std::string root_id_; + std::string vm_id_; + std::string vm_configuration_; + std::string vm_key_; + std::string plugin_configuration_; + std::shared_ptr plugin_; + std::shared_ptr wasm_; +}; + +#if defined(ENVOY_WASM_V8) || defined(ENVOY_WASM_WAVM) +class WasmTest : public WasmTestBase, public testing::TestWithParam { +public: + void createWasm() { WasmTestBase::createWasm(GetParam()); } +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8" +#endif +#if defined(ENVOY_WASM_V8) && defined(ENVOY_WASM_WAVM) + , +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm" +#endif +); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmTest, testing_values); +#endif + +class WasmNullTest : public WasmTestBase, public testing::TestWithParam { +public: + void createWasm() { + WasmTestBase::createWasm(GetParam()); + const auto code = + GetParam() != "null" + ? TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/stats_cpp.wasm")) + : "WasmStatsCpp"; + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm_->initialize(code, false)); + } +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_null_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmNullTest, testing_null_values); + +#if defined(ENVOY_WASM_V8) || defined(ENVOY_WASM_WAVM) +class WasmTestMatrix : public WasmTestBase, + public testing::TestWithParam> { +public: + void createWasm() { WasmTestBase::createWasm(std::get<0>(GetParam())); } + + void setWasmCode(std::string vm_configuration) { + const auto basic_path = + absl::StrCat("test/extensions/bootstrap/wasm/test_data/", vm_configuration); + code_ = TestEnvironment::readFileToStringForTest( + TestEnvironment::runfilesPath(basic_path + "_" + std::get<1>(GetParam()) + ".wasm")); + + EXPECT_FALSE(code_.empty()); + } + +protected: + std::string code_; +}; + +INSTANTIATE_TEST_SUITE_P(RuntimesAndLanguages, WasmTestMatrix, + testing::Combine(testing::Values( +#if defined(ENVOY_WASM_V8) + "v8" +#endif +#if defined(ENVOY_WASM_V8) && defined(ENVOY_WASM_WAVM) + , +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm" +#endif + ), + testing::Values("cpp", "rust"))); + +TEST_P(WasmTestMatrix, Logging) { + plugin_configuration_ = "configure-test"; + createWasm(); + setWasmCode("logging"); + + auto wasm_weak = std::weak_ptr(wasm_); + auto wasm_handler = std::make_unique(std::move(wasm_)); + + EXPECT_TRUE(wasm_weak.lock()->initialize(code_, false)); + auto context = static_cast(wasm_weak.lock()->start(plugin_)); + + if (std::get<1>(GetParam()) == "cpp") { + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("printf stdout test"))); + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("printf stderr test"))); + } + EXPECT_CALL(*context, log_(spdlog::level::warn, Eq("warn configure-test"))); + EXPECT_CALL(*context, log_(spdlog::level::trace, Eq("test trace logging"))); + EXPECT_CALL(*context, log_(spdlog::level::debug, Eq("test debug logging"))); + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("test error logging"))); + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("test tick logging"))) + .Times(testing::AtLeast(1)); + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("onDone logging"))); + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("onDelete logging"))); + + EXPECT_TRUE(wasm_weak.lock()->configure(context, plugin_)); + wasm_handler.reset(); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + // This will `SEGV` on nullptr if wasm has been deleted. + context->onTick(0); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + dispatcher_->clearDeferredDeleteList(); +} +#endif + +#if defined(ENVOY_WASM_V8) || defined(ENVOY_WASM_WAVM) +TEST_P(WasmTest, BadSignature) { + createWasm(); + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/bad_signature_cpp.wasm")); + EXPECT_FALSE(code.empty()); + EXPECT_FALSE(wasm_->initialize(code, false)); + EXPECT_TRUE(wasm_->isFailed()); +} + +TEST_P(WasmTest, Segv) { + createWasm(); + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/segv_cpp.wasm")); + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm_->initialize(code, false)); + auto context = static_cast(wasm_->start(plugin_)); + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("before badptr"))); + EXPECT_FALSE(wasm_->configure(context, plugin_)); + wasm_->isFailed(); +} + +TEST_P(WasmTest, DivByZero) { + createWasm(); + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/segv_cpp.wasm")); + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm_->initialize(code, false)); + auto context = static_cast(wasm_->start(plugin_)); + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("before div by zero"))); + context->onLog(); + wasm_->isFailed(); +} + +TEST_P(WasmTest, EmscriptenVersion) { + createWasm(); + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/segv_cpp.wasm")); + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm_->initialize(code, false)); + uint32_t major = 9, minor = 9, abi_major = 9, abi_minor = 9; + EXPECT_TRUE(wasm_->getEmscriptenVersion(&major, &minor, &abi_major, &abi_minor)); + EXPECT_EQ(major, 0); + EXPECT_LE(minor, 3); + // Up to (at least) emsdk 1.39.6. + EXPECT_EQ(abi_major, 0); + EXPECT_LE(abi_minor, 20); +} + +TEST_P(WasmTest, IntrinsicGlobals) { + createWasm(); + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/emscripten_cpp.wasm")); + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm_->initialize(code, false)); + auto context = static_cast(wasm_->start(plugin_)); + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("NaN nan"))); + EXPECT_CALL(*context, log_(spdlog::level::warn, Eq("inf inf"))).Times(3); + EXPECT_TRUE(wasm_->configure(context, plugin_)); +} + +// The `asm2wasm.wasm` file uses operations which would require the `asm2wasm` Emscripten module +// *if* em++ is invoked with the trap mode "clamp". See +// https://emscripten.org/docs/compiling/WebAssembly.html This test demonstrates that the `asm2wasm` +// module is not required with the trap mode is set to "allow". Note: future Wasm standards will +// change this behavior by providing non-trapping instructions, but in the mean time we support the +// default Emscripten behavior. +TEST_P(WasmTest, Asm2Wasm) { + createWasm(); + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/asm2wasm_cpp.wasm")); + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm_->initialize(code, false)); + auto context = static_cast(wasm_->start(plugin_)); + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("out 0 0 0"))); + EXPECT_TRUE(wasm_->configure(context, plugin_)); +} +#endif + +TEST_P(WasmNullTest, Stats) { + createWasm(); + auto context = static_cast(wasm_->start(plugin_)); + + EXPECT_CALL(*context, log_(spdlog::level::trace, Eq("get counter = 1"))); + EXPECT_CALL(*context, log_(spdlog::level::debug, Eq("get counter = 2"))); + // recordMetric on a Counter is the same as increment. + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("get counter = 5"))); + EXPECT_CALL(*context, log_(spdlog::level::warn, Eq("get gauge = 2"))); + // Get is not supported on histograms. + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("get histogram = Unsupported"))); + + EXPECT_TRUE(wasm_->configure(context, plugin_)); + EXPECT_EQ(scope_->counterFromString("test_counter").value(), 5); + EXPECT_EQ(scope_->gaugeFromString("test_gauge", Stats::Gauge::ImportMode::Accumulate).value(), 2); +} + +TEST_P(WasmNullTest, StatsHigherLevel) { + createWasm(); + auto context = static_cast(wasm_->start(plugin_)); + + EXPECT_CALL(*context, log_(spdlog::level::trace, Eq("get counter = 1"))); + EXPECT_CALL(*context, log_(spdlog::level::debug, Eq("get counter = 2"))); + // recordMetric on a Counter is the same as increment. + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("get counter = 5"))); + EXPECT_CALL(*context, log_(spdlog::level::warn, Eq("get gauge = 2"))); + // Get is not supported on histograms. + EXPECT_CALL(*context, log_(spdlog::level::err, + Eq(std::string("resolved histogram name = " + "histogram_int_tag.7.histogram_string_tag.test_tag." + "histogram_bool_tag.true.test_histogram")))); + + wasm_->setTimerPeriod(1, std::chrono::milliseconds(10)); + wasm_->tickHandler(1); + EXPECT_EQ(scope_->counterFromString("counter_tag.test_tag.test_counter").value(), 5); + EXPECT_EQ( + scope_->gaugeFromString("gauge_int_tag.9.test_gauge", Stats::Gauge::ImportMode::Accumulate) + .value(), + 2); +} + +TEST_P(WasmNullTest, StatsHighLevel) { + createWasm(); + auto context = static_cast(wasm_->start(plugin_)); + + EXPECT_CALL(*context, log_(spdlog::level::trace, Eq("get counter = 1"))); + EXPECT_CALL(*context, log_(spdlog::level::debug, Eq("get counter = 2"))); + // recordMetric on a Counter is the same as increment. + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("get counter = 5"))); + EXPECT_CALL(*context, log_(spdlog::level::warn, Eq("get gauge = 2"))); + // Get is not supported on histograms. + // EXPECT_CALL(*context, log_(spdlog::level::err, Eq(std::string("resolved histogram name + // = int_tag.7_string_tag.test_tag.bool_tag.true.test_histogram")))); + EXPECT_CALL(*context, + log_(spdlog::level::err, + Eq("h_id = int_tag.7.string_tag.test_tag.bool_tag.true.test_histogram"))); + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("stack_c = 1"))); + EXPECT_CALL(*context, log_(spdlog::level::err, Eq("stack_g = 2"))); + // Get is not supported on histograms. + // EXPECT_CALL(*context, log_(spdlog::level::err, Eq("stack_h = 3"))); + context->onLog(); + EXPECT_EQ( + scope_->counterFromString("string_tag.test_tag.int_tag.7.bool_tag.true.test_counter").value(), + 5); + EXPECT_EQ(scope_ + ->gaugeFromString("string_tag1.test_tag1.string_tag2.test_tag2.test_gauge", + Stats::Gauge::ImportMode::Accumulate) + .value(), + 2); +} + +} // namespace Wasm +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/wasm/BUILD b/test/extensions/common/wasm/BUILD index e85cf73322e4..0a8dfd1b8247 100644 --- a/test/extensions/common/wasm/BUILD +++ b/test/extensions/common/wasm/BUILD @@ -1,8 +1,13 @@ load( "//bazel:envoy_build_system.bzl", "envoy_cc_test", + "envoy_cc_test_binary", "envoy_package", ) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm", +) licenses(["notice"]) # Apache 2 @@ -11,15 +16,59 @@ envoy_package() envoy_cc_test( name = "wasm_vm_test", srcs = ["wasm_vm_test.cc"], - data = [ - "//test/extensions/common/wasm/test_data:modules", + data = envoy_select_wasm([ + "//test/extensions/common/wasm/test_data:test_rust.wasm", + ]), + tags = [ + # wasm (wee v8 etc) will not compile on Windows + "skip_on_windows", ], - # wasm (wee v8 etc) will not compile on Windows - tags = ["skip_on_windows"], deps = [ - "//source/extensions/common/wasm:wasm_vm_lib", + "//source/extensions/common/wasm:wasm_lib", "//test/test_common:environment_lib", "//test/test_common:registry_lib", "//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "wasm_test", + srcs = ["wasm_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/common/wasm/test_data:bad_signature_cpp.wasm", + "//test/extensions/common/wasm/test_data:test_context_cpp.wasm", + "//test/extensions/common/wasm/test_data:test_cpp.wasm", + ]), + external_deps = ["abseil_optional"], + deps = [ + "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", + "//source/common/event:dispatcher_lib", + "//source/common/stats:isolated_store_lib", + "//source/common/stats:stats_lib", + "//source/extensions/common/crypto:utility_lib", + "//source/extensions/common/wasm:wasm_lib", + "//test/extensions/common/wasm/test_data:test_context_cpp_plugin", + "//test/extensions/common/wasm/test_data:test_cpp_plugin", + "//test/mocks/server:server_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:wasm_lib", + ], +) + +envoy_cc_test_binary( + name = "wasm_speed_test", + srcs = ["wasm_speed_test.cc"], + external_deps = [ + "abseil_optional", + "benchmark", + ], + deps = [ + "//source/common/event:dispatcher_lib", + "//source/extensions/common/wasm:wasm_lib", + "//test/mocks/server:server_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", + ], +) diff --git a/test/extensions/common/wasm/test_data/BUILD b/test/extensions/common/wasm/test_data/BUILD index f46c28bbd63e..b40b3e49e41b 100644 --- a/test/extensions/common/wasm/test_data/BUILD +++ b/test/extensions/common/wasm/test_data/BUILD @@ -1,13 +1,75 @@ load( "//bazel:envoy_build_system.bzl", + "envoy_cc_library", "envoy_package", ) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary", "wasm_rust_binary") licenses(["notice"]) # Apache 2 envoy_package() -filegroup( - name = "modules", - srcs = glob(["*.wasm"]), +wasm_rust_binary( + name = "test_rust.wasm", + srcs = ["test_rust.rs"], + rustc_flags = ["-Clink-arg=-zstack-size=32768"], +) + +envoy_cc_library( + name = "test_cpp_plugin", + srcs = [ + "test_cpp.cc", + "test_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_cc_library( + name = "test_context_cpp_plugin", + srcs = [ + "test_context_cpp.cc", + "test_context_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + "//source/extensions/common/wasm/ext:envoy_null_plugin", + ], +) + +envoy_wasm_cc_binary( + name = "test_cpp.wasm", + srcs = ["test_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) + +envoy_wasm_cc_binary( + name = "test_context_cpp.wasm", + srcs = ["test_context_cpp.cc"], + deps = [ + "//source/extensions/common/wasm/ext:envoy_proxy_wasm_api_lib", + ], +) + +envoy_wasm_cc_binary( + name = "bad_signature_cpp.wasm", + srcs = ["bad_signature_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], ) diff --git a/test/extensions/common/wasm/test_data/bad_signature_cpp.cc b/test/extensions/common/wasm/test_data/bad_signature_cpp.cc new file mode 100644 index 000000000000..918024581557 --- /dev/null +++ b/test/extensions/common/wasm/test_data/bad_signature_cpp.cc @@ -0,0 +1,16 @@ +// NOLINT(namespace-envoy) +#include + +#define EMSCRIPTEN_KEEPALIVE __attribute__((used)) __attribute__((visibility("default"))) + +// Required Proxy-Wasm ABI version. +extern "C" EMSCRIPTEN_KEEPALIVE void proxy_abi_version_0_1_0() {} + +extern "C" uint32_t proxy_log(uint32_t level, const char* logMessage, size_t messageSize); + +extern "C" EMSCRIPTEN_KEEPALIVE uint32_t proxy_on_configure(uint32_t, int bad, char* configuration, + int size) { + std::string message = "bad signature"; + proxy_log(4 /* error */, message.c_str(), message.size()); + return 1; +} diff --git a/test/extensions/common/wasm/test_data/test_context_cpp.cc b/test/extensions/common/wasm/test_data/test_context_cpp.cc new file mode 100644 index 000000000000..c89164e43f11 --- /dev/null +++ b/test/extensions/common/wasm/test_data/test_context_cpp.cc @@ -0,0 +1,82 @@ +// NOLINT(namespace-envoy) +#include +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#include "source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(CommonWasmTestContextCpp) + +class TestContext : public EnvoyContext { +public: + explicit TestContext(uint32_t id, RootContext* root) : EnvoyContext(id, root) {} +}; + +class TestRootContext : public EnvoyRootContext { +public: + explicit TestRootContext(uint32_t id, std::string_view root_id) : EnvoyRootContext(id, root_id) {} + + bool onStart(size_t vm_configuration_size) override; + bool onDone() override; + void onTick() override; + void onQueueReady(uint32_t) override; + void onResolveDns(uint32_t token, uint32_t results_size) override; + +private: + uint32_t dns_token_; +}; + +static RegisterContextFactory register_TestContext(CONTEXT_FACTORY(TestContext), + ROOT_FACTORY(TestRootContext)); +static RegisterContextFactory register_EmptyTestContext(CONTEXT_FACTORY(EnvoyContext), + ROOT_FACTORY(EnvoyRootContext), "empty"); + +bool TestRootContext::onStart(size_t) { + envoy_resolve_dns("example.com", sizeof("example.com") - 1, &dns_token_); + return true; +} + +void TestRootContext::onResolveDns(uint32_t token, uint32_t result_size) { + logWarn("TestRootContext::onResolveDns " + std::to_string(token)); + auto dns_buffer = getBufferBytes(WasmBufferType::CallData, 0, result_size); + auto dns = parseDnsResults(dns_buffer->view()); + for (auto& e : dns) { + logInfo("TestRootContext::onResolveDns dns " + std::to_string(e.ttl_seconds) + " " + e.address); + } +} + +bool TestRootContext::onDone() { + logWarn("TestRootContext::onDone " + std::to_string(id())); + return true; +} + +// Null VM fails on nullptr. +void TestRootContext::onTick() { + if (envoy_resolve_dns(0, 1, &dns_token_) != WasmResult::InvalidMemoryAccess) { + logInfo("resolve_dns should report invalid memory access"); + } + if (envoy_resolve_dns("example.com", sizeof("example.com") - 1, nullptr) != + WasmResult::InvalidMemoryAccess) { + logInfo("resolve_dns should report invalid memory access"); + } +} + +// V8 fails on pointer too large. +void TestRootContext::onQueueReady(uint32_t) { + if (envoy_resolve_dns(reinterpret_cast(INT_MAX), 0, &dns_token_) != + WasmResult::InvalidMemoryAccess) { + logInfo("resolve_dns should report invalid memory access"); + } + if (envoy_resolve_dns("example.com", sizeof("example.com") - 1, + reinterpret_cast(INT_MAX)) != WasmResult::InvalidMemoryAccess) { + logInfo("resolve_dns should report invalid memory access"); + } +} + +END_WASM_PLUGIN diff --git a/test/extensions/common/wasm/test_data/test_context_cpp_null_plugin.cc b/test/extensions/common/wasm/test_data/test_context_cpp_null_plugin.cc new file mode 100644 index 000000000000..88e3a18943f0 --- /dev/null +++ b/test/extensions/common/wasm/test_data/test_context_cpp_null_plugin.cc @@ -0,0 +1,16 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace CommonWasmTestContextCpp { +NullPluginRegistry* context_registry_; +} // namespace CommonWasmTestContextCpp + +RegisterNullVmPluginFactory + register_common_wasm_test_context_cpp_plugin("CommonWasmTestContextCpp", []() { + return std::make_unique(CommonWasmTestContextCpp::context_registry_); + }); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/common/wasm/test_data/test_cpp.cc b/test/extensions/common/wasm/test_data/test_cpp.cc new file mode 100644 index 000000000000..1d990901846a --- /dev/null +++ b/test/extensions/common/wasm/test_data/test_cpp.cc @@ -0,0 +1,275 @@ +// NOLINT(namespace-envoy) +#ifndef WIN32 +#include "unistd.h" + +#endif +#include +#include +#include +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#else +#include "include/proxy-wasm/null_plugin.h" +#endif + +START_WASM_PLUGIN(CommonWasmTestCpp) + +static int* badptr = nullptr; +static float gNan = std::nan("1"); +static float gInfinity = INFINITY; +volatile double zero_unbeknownst_to_the_compiler = 0.0; + +#ifndef CHECK_RESULT +#define CHECK_RESULT(_c) \ + do { \ + if ((_c) != WasmResult::Ok) { \ + proxy_log(LogLevel::critical, #_c, sizeof(#_c) - 1); \ + abort(); \ + } \ + } while (0) +#endif + +#define CHECK_RESULT_NOT_OK(_c) \ + do { \ + if ((_c) == WasmResult::Ok) { \ + proxy_log(LogLevel::critical, #_c, sizeof(#_c) - 1); \ + abort(); \ + } \ + } while (0) + +#define FAIL_NOW(_msg) \ + do { \ + const std::string __message = _msg; \ + proxy_log(LogLevel::critical, __message.c_str(), __message.size()); \ + abort(); \ + } while (0) + +WASM_EXPORT(void, proxy_abi_version_0_2_1, (void)) {} + +WASM_EXPORT(void, proxy_on_context_create, (uint32_t, uint32_t)) {} + +WASM_EXPORT(uint32_t, proxy_on_vm_start, (uint32_t context_id, uint32_t configuration_size)) { + const char* configuration_ptr = nullptr; + size_t size; + proxy_get_buffer_bytes(WasmBufferType::VmConfiguration, 0, configuration_size, &configuration_ptr, + &size); + std::string configuration(configuration_ptr, size); + if (configuration == "logging") { + std::string trace_message = "test trace logging"; + proxy_log(LogLevel::trace, trace_message.c_str(), trace_message.size()); + std::string debug_message = "test debug logging"; + proxy_log(LogLevel::debug, debug_message.c_str(), debug_message.size()); + std::string warn_message = "test warn logging"; + proxy_log(LogLevel::warn, warn_message.c_str(), warn_message.size()); + std::string error_message = "test error logging"; + proxy_log(LogLevel::error, error_message.c_str(), error_message.size()); + LogLevel log_level; + CHECK_RESULT(proxy_get_log_level(&log_level)); + std::string level_message = "log level is " + std::to_string(static_cast(log_level)); + proxy_log(LogLevel::info, level_message.c_str(), level_message.size()); + } else if (configuration == "segv") { + std::string message = "before badptr"; + proxy_log(LogLevel::error, message.c_str(), message.size()); + ::free(const_cast(reinterpret_cast(configuration_ptr))); + *badptr = 1; + message = "after badptr"; + proxy_log(LogLevel::error, message.c_str(), message.size()); + } else if (configuration == "divbyzero") { + std::string message = "before div by zero"; + proxy_log(LogLevel::error, message.c_str(), message.size()); + ::free(const_cast(reinterpret_cast(configuration_ptr))); + int zero = context_id & 0x100000; + message = "divide by zero: " + std::to_string(100 / zero); + proxy_log(LogLevel::error, message.c_str(), message.size()); + } else if (configuration == "globals") { + std::string message = "NaN " + std::to_string(gNan); + proxy_log(LogLevel::warn, message.c_str(), message.size()); + message = "inf " + std::to_string(gInfinity); + proxy_log(LogLevel::warn, message.c_str(), message.size()); + message = "inf " + std::to_string(1.0 / zero_unbeknownst_to_the_compiler); + proxy_log(LogLevel::warn, message.c_str(), message.size()); + message = std::string("inf ") + (std::isinf(gInfinity) ? "inf" : "nan"); + proxy_log(LogLevel::warn, message.c_str(), message.size()); + } else if (configuration == "stats") { + uint32_t c, g, h; + + std::string name = "test_counter"; + CHECK_RESULT(proxy_define_metric(MetricType::Counter, name.data(), name.size(), &c)); + name = "test_gauge"; + CHECK_RESULT(proxy_define_metric(MetricType::Gauge, name.data(), name.size(), &g)); + name = "test_historam"; + CHECK_RESULT(proxy_define_metric(MetricType::Histogram, name.data(), name.size(), &h)); + // Bad type. + CHECK_RESULT_NOT_OK( + proxy_define_metric(static_cast(9999), name.data(), name.size(), &c)); + + CHECK_RESULT(proxy_increment_metric(c, 1)); + CHECK_RESULT(proxy_increment_metric(g, 1)); + CHECK_RESULT_NOT_OK(proxy_increment_metric(h, 1)); + CHECK_RESULT(proxy_record_metric(g, 2)); + CHECK_RESULT(proxy_record_metric(h, 3)); + + uint64_t value; + // Not found + CHECK_RESULT_NOT_OK(proxy_get_metric((1 << 10) + 0, &value)); + CHECK_RESULT_NOT_OK(proxy_get_metric((1 << 10) + 1, &value)); + CHECK_RESULT_NOT_OK(proxy_get_metric((1 << 10) + 2, &value)); + CHECK_RESULT_NOT_OK(proxy_get_metric((1 << 10) + 3, &value)); + CHECK_RESULT_NOT_OK(proxy_record_metric((1 << 10) + 0, 1)); + CHECK_RESULT_NOT_OK(proxy_record_metric((1 << 10) + 1, 1)); + CHECK_RESULT_NOT_OK(proxy_record_metric((1 << 10) + 2, 1)); + CHECK_RESULT_NOT_OK(proxy_record_metric((1 << 10) + 3, 1)); + CHECK_RESULT_NOT_OK(proxy_increment_metric((1 << 10) + 0, 1)); + CHECK_RESULT_NOT_OK(proxy_increment_metric((1 << 10) + 1, 1)); + CHECK_RESULT_NOT_OK(proxy_increment_metric((1 << 10) + 2, 1)); + CHECK_RESULT_NOT_OK(proxy_increment_metric((1 << 10) + 3, 1)); + // Found. + std::string message; + CHECK_RESULT(proxy_get_metric(c, &value)); + message = std::string("get counter = ") + std::to_string(value); + proxy_log(LogLevel::trace, message.c_str(), message.size()); + CHECK_RESULT(proxy_increment_metric(c, 1)); + CHECK_RESULT(proxy_get_metric(c, &value)); + message = std::string("get counter = ") + std::to_string(value); + proxy_log(LogLevel::debug, message.c_str(), message.size()); + CHECK_RESULT(proxy_record_metric(c, 3)); + CHECK_RESULT(proxy_get_metric(c, &value)); + message = std::string("get counter = ") + std::to_string(value); + proxy_log(LogLevel::info, message.c_str(), message.size()); + CHECK_RESULT(proxy_get_metric(g, &value)); + message = std::string("get gauge = ") + std::to_string(value); + proxy_log(LogLevel::warn, message.c_str(), message.size()); + // Get on histograms is not supported. + if (proxy_get_metric(h, &value) != WasmResult::Ok) { + message = std::string("get histogram = Unsupported"); + proxy_log(LogLevel::error, message.c_str(), message.size()); + } + // Negative. + CHECK_RESULT_NOT_OK(proxy_increment_metric(c, -1)); + CHECK_RESULT(proxy_increment_metric(g, -1)); + } else if (configuration == "foreign") { + std::string function = "compress"; + char* compressed = nullptr; + size_t compressed_size = 0; + std::string argument = std::string(2000, 'a'); // super compressible. + std::string message; + CHECK_RESULT(proxy_call_foreign_function(function.data(), function.size(), argument.data(), + argument.size(), &compressed, &compressed_size)); + message = std::string("compress ") + std::to_string(argument.size()) + " -> " + + std::to_string(compressed_size); + proxy_log(LogLevel::trace, message.c_str(), message.size()); + function = "uncompress"; + char* result = nullptr; + size_t result_size = 0; + CHECK_RESULT(proxy_call_foreign_function(function.data(), function.size(), compressed, + compressed_size, &result, &result_size)); + message = std::string("uncompress ") + std::to_string(compressed_size) + " -> " + + std::to_string(result_size); + proxy_log(LogLevel::debug, message.c_str(), message.size()); + if (argument != std::string(result, result_size)) { + message = "compress mismatch "; + proxy_log(LogLevel::error, message.c_str(), message.size()); + } + ::free(result); + result = nullptr; + memset(compressed, 0, 4); // damage the compressed version. + if (proxy_call_foreign_function(function.data(), function.size(), compressed, compressed_size, + &result, &result_size) != WasmResult::SerializationFailure) { + message = "bad uncompress should be an error"; + proxy_log(LogLevel::error, message.c_str(), message.size()); + } + if (compressed) { + ::free(compressed); + } + if (result) { + ::free(result); + } + } else if (configuration == "configuration") { + std::string message = "configuration"; + proxy_log(LogLevel::error, message.c_str(), message.size()); + } else if (configuration == "WASI") { + // These checks depend on Emscripten's support for `WASI` and will only + // work if invoked on a "real" Wasm VM. + int err = fprintf(stdout, "WASI write to stdout\n"); + if (err < 0) { + FAIL_NOW("stdout write should succeed"); + } + err = fprintf(stderr, "WASI write to stderr\n"); + if (err < 0) { + FAIL_NOW("stderr write should succeed"); + } + // We explicitly don't support reading from stdin + char tmp[16]; + size_t rc = fread(static_cast(tmp), 1, 16, stdin); + if (rc != 0 || errno != ENOSYS) { + FAIL_NOW("stdin read should fail. errno = " + std::to_string(errno)); + } + // No environment variables should be available + char* pathenv = getenv("PATH"); + if (pathenv != nullptr) { + FAIL_NOW("PATH environment variable should not be available"); + } +#ifndef WIN32 + // Exercise the `WASI` `fd_fdstat_get` a little bit + int tty = isatty(1); + if (errno != ENOTTY || tty != 0) { + FAIL_NOW("stdout is not a tty"); + } + tty = isatty(2); + if (errno != ENOTTY || tty != 0) { + FAIL_NOW("stderr is not a tty"); + } + tty = isatty(99); + if (errno != EBADF || tty != 0) { + FAIL_NOW("isatty errors on bad fds. errno = " + std::to_string(errno)); + } +#endif + } else if (configuration == "on_foreign") { + std::string message = "on_foreign start"; + proxy_log(LogLevel::debug, message.c_str(), message.size()); + } else { + std::string message = "on_vm_start " + configuration; + proxy_log(LogLevel::info, message.c_str(), message.size()); + } + ::free(const_cast(reinterpret_cast(configuration_ptr))); + return 1; +} + +WASM_EXPORT(uint32_t, proxy_on_configure, (uint32_t, uint32_t configuration_size)) { + const char* configuration_ptr = nullptr; + size_t size; + proxy_get_buffer_bytes(WasmBufferType::PluginConfiguration, 0, configuration_size, + &configuration_ptr, &size); + std::string configuration(configuration_ptr, size); + if (configuration == "done") { + proxy_done(); + } else { + std::string message = "on_configuration " + configuration; + proxy_log(LogLevel::info, message.c_str(), message.size()); + } + ::free(const_cast(reinterpret_cast(configuration_ptr))); + return 1; +} + +WASM_EXPORT(void, proxy_on_foreign_function, (uint32_t, uint32_t token, uint32_t data_size)) { + std::string message = + "on_foreign_function " + std::to_string(token) + " " + std::to_string(data_size); + proxy_log(LogLevel::info, message.c_str(), message.size()); +} + +WASM_EXPORT(uint32_t, proxy_on_done, (uint32_t)) { + std::string message = "on_done logging"; + proxy_log(LogLevel::info, message.c_str(), message.size()); + return 0; +} + +WASM_EXPORT(void, proxy_on_delete, (uint32_t)) { + std::string message = "on_delete logging"; + proxy_log(LogLevel::info, message.c_str(), message.size()); +} + +END_WASM_PLUGIN diff --git a/test/extensions/common/wasm/test_data/test_cpp_null_plugin.cc b/test/extensions/common/wasm/test_data/test_cpp_null_plugin.cc new file mode 100644 index 000000000000..d8665f7b28c0 --- /dev/null +++ b/test/extensions/common/wasm/test_data/test_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace CommonWasmTestCpp { +NullPluginRegistry* context_registry_; +} // namespace CommonWasmTestCpp + +RegisterNullVmPluginFactory register_common_wasm_test_cpp_plugin("CommonWasmTestCpp", []() { + return std::make_unique(CommonWasmTestCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/common/wasm/test_data/test_rust.wasm b/test/extensions/common/wasm/test_data/test_rust.wasm deleted file mode 100755 index 2de74d763a6c..000000000000 Binary files a/test/extensions/common/wasm/test_data/test_rust.wasm and /dev/null differ diff --git a/test/extensions/common/wasm/wasm_speed_test.cc b/test/extensions/common/wasm/wasm_speed_test.cc new file mode 100644 index 000000000000..af1c31a2408f --- /dev/null +++ b/test/extensions/common/wasm/wasm_speed_test.cc @@ -0,0 +1,82 @@ +#include "common/common/thread.h" +#include "common/common/thread_synchronizer.h" + +#include "extensions/common/wasm/wasm.h" + +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/thread_factory_for_test.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_cat.h" +#include "absl/synchronization/notification.h" +#include "benchmark/benchmark.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "tools/cpp/runfiles/runfiles.h" + +using bazel::tools::cpp::runfiles::Runfiles; + +namespace Envoy { + +void bmWasmSpeedTest(benchmark::State& state) { + Envoy::Thread::MutexBasicLockable lock; + Envoy::Logger::Context logging_state(spdlog::level::warn, + Envoy::Logger::Logger::DEFAULT_LOG_FORMAT, lock, false); + Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm).set_level(spdlog::level::off); + Envoy::Stats::IsolatedStoreImpl stats_store; + Envoy::Api::ApiPtr api = Envoy::Api::createApiForTest(stats_store); + Envoy::Upstream::MockClusterManager cluster_manager; + Envoy::Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Envoy::Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + auto wasm = std::make_unique( + "envoy.wasm.runtime.null", "", "", "", scope, cluster_manager, *dispatcher); + + auto context = std::make_shared(wasm.get()); + Envoy::Thread::ThreadFactory& thread_factory{Envoy::Thread::threadFactoryForTest()}; + std::pair data; + int n_threads = 10; + + for (__attribute__((unused)) auto _ : state) { + auto thread_fn = [&]() { + for (int i = 0; i < 1000000; i++) { + context->getSharedData("foo", &data); + context->setSharedData("foo", "bar", 1); + } + return new uint32_t(42); + }; + std::vector threads; + for (int i = 0; i < n_threads; ++i) { + std::string name = absl::StrCat("thread", i); + threads.emplace_back(thread_factory.createThread(thread_fn, Envoy::Thread::Options{name})); + } + for (auto& thread : threads) { + thread->join(); + } + } +} + +BENCHMARK(bmWasmSpeedTest); + +} // namespace Envoy + +int main(int argc, char** argv) { + ::benchmark::Initialize(&argc, argv); + Envoy::TestEnvironment::initializeOptions(argc, argv); + // Create a Runfiles object for runfiles lookup. + // https://github.com/bazelbuild/bazel/blob/master/tools/cpp/runfiles/runfiles_src.h#L32 + std::string error; + std::unique_ptr runfiles(Runfiles::Create(argv[0], &error)); + RELEASE_ASSERT(Envoy::TestEnvironment::getOptionalEnvVar("NORUNFILES").has_value() || + runfiles != nullptr, + error); + Envoy::TestEnvironment::setRunfiles(runfiles.get()); + Envoy::TestEnvironment::setEnvVar("ENVOY_IP_TEST_VERSIONS", "all", 0); + Envoy::Event::Libevent::Global::initialize(); + if (::benchmark::ReportUnrecognizedArguments(argc, argv)) { + return 1; + } + ::benchmark::RunSpecifiedBenchmarks(); + return 0; +} diff --git a/test/extensions/common/wasm/wasm_test.cc b/test/extensions/common/wasm/wasm_test.cc new file mode 100644 index 000000000000..f17d0ca859d6 --- /dev/null +++ b/test/extensions/common/wasm/wasm_test.cc @@ -0,0 +1,1062 @@ +#include "envoy/server/lifecycle_notifier.h" + +#include "common/common/hex.h" +#include "common/event/dispatcher_impl.h" +#include "common/stats/isolated_store_impl.h" + +#include "extensions/common/wasm/wasm.h" + +#include "test/mocks/server/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" +#include "test/test_common/wasm_base.h" + +#include "absl/types/optional.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "openssl/bytestring.h" +#include "openssl/hmac.h" +#include "openssl/sha.h" + +using Envoy::Server::ServerLifecycleNotifier; +using StageCallbackWithCompletion = + Envoy::Server::ServerLifecycleNotifier::StageCallbackWithCompletion; +using testing::Eq; +using testing::Return; + +namespace Envoy { + +namespace Server { +class MockServerLifecycleNotifier2 : public ServerLifecycleNotifier { +public: + MockServerLifecycleNotifier2() = default; + ~MockServerLifecycleNotifier2() override = default; + + using ServerLifecycleNotifier::registerCallback; + + ServerLifecycleNotifier::HandlePtr + registerCallback(Stage stage, StageCallbackWithCompletion callback) override { + return registerCallback2(stage, callback); + } + + MOCK_METHOD(ServerLifecycleNotifier::HandlePtr, registerCallback, (Stage, StageCallback)); + MOCK_METHOD(ServerLifecycleNotifier::HandlePtr, registerCallback2, + (Stage stage, StageCallbackWithCompletion callback)); +}; +} // namespace Server + +namespace Extensions { +namespace Common { +namespace Wasm { + +REGISTER_WASM_EXTENSION(EnvoyWasm); + +std::string sha256(absl::string_view data) { + std::vector digest(SHA256_DIGEST_LENGTH); + EVP_MD_CTX* ctx(EVP_MD_CTX_new()); + auto rc = EVP_DigestInit(ctx, EVP_sha256()); + RELEASE_ASSERT(rc == 1, "Failed to init digest context"); + rc = EVP_DigestUpdate(ctx, data.data(), data.size()); + RELEASE_ASSERT(rc == 1, "Failed to update digest"); + rc = EVP_DigestFinal(ctx, digest.data(), nullptr); + RELEASE_ASSERT(rc == 1, "Failed to finalize digest"); + EVP_MD_CTX_free(ctx); + return std::string(reinterpret_cast(&digest[0]), digest.size()); +} + +class TestContext : public ::Envoy::Extensions::Common::Wasm::Context { +public: + using ::Envoy::Extensions::Common::Wasm::Context::Context; + ~TestContext() override = default; + using ::Envoy::Extensions::Common::Wasm::Context::log; + proxy_wasm::WasmResult log(uint32_t level, absl::string_view message) override { + std::cerr << std::string(message) << "\n"; + log_(static_cast(level), message); + Extensions::Common::Wasm::Context::log(static_cast(level), message); + return proxy_wasm::WasmResult::Ok; + } + MOCK_METHOD2(log_, void(spdlog::level::level_enum level, absl::string_view message)); +}; + +class WasmCommonTest : public testing::TestWithParam { +public: + void SetUp() override { // NOLINT(readability-identifier-naming) + Logger::Registry::getLog(Logger::Id::wasm).set_level(spdlog::level::debug); + clearCodeCacheForTesting(); + } +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto test_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmCommonTest, test_values); + +TEST_P(WasmCommonTest, EnvoyWasm) { + auto envoy_wasm = std::make_unique(); + envoy_wasm->initialize(); + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto plugin = std::make_shared( + "", "", "", GetParam(), "", false, envoy::config::core::v3::TrafficDirection::UNSPECIFIED, + local_info, nullptr); + auto wasm = std::make_shared( + std::make_unique(absl::StrCat("envoy.wasm.runtime.", GetParam()), "", + "vm_configuration", "", scope, cluster_manager, *dispatcher)); + auto wasm_base = std::dynamic_pointer_cast(wasm); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::UnableToCreateVM); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::UnableToCreateVM); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::UnableToCloneVM); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::UnableToCloneVM); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::MissingFunction); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::MissingFunction); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::UnableToInitializeCode); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::UnableToInitializeCode); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::StartFailed); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::StartFailed); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::ConfigureFailed); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::ConfigureFailed); + wasm->wasm()->setFailStateForTesting(proxy_wasm::FailState::RuntimeError); + EXPECT_EQ(toWasmEvent(wasm_base), EnvoyWasm::WasmEvent::RuntimeError); + + auto root_context = static_cast(wasm->wasm()->createRootContext(plugin)); + uint32_t grpc_call_token1 = root_context->nextGrpcCallToken(); + uint32_t grpc_call_token2 = root_context->nextGrpcCallToken(); + EXPECT_NE(grpc_call_token1, grpc_call_token2); + root_context->setNextGrpcTokenForTesting(0); // Rollover. + EXPECT_EQ(root_context->nextGrpcCallToken(), 1); + + uint32_t grpc_stream_token1 = root_context->nextGrpcStreamToken(); + uint32_t grpc_stream_token2 = root_context->nextGrpcStreamToken(); + EXPECT_NE(grpc_stream_token1, grpc_stream_token2); + root_context->setNextGrpcTokenForTesting(0xFFFFFFFF); // Rollover. + EXPECT_EQ(root_context->nextGrpcStreamToken(), 2); + + uint32_t http_call_token1 = root_context->nextHttpCallToken(); + uint32_t http_call_token2 = root_context->nextHttpCallToken(); + EXPECT_NE(http_call_token1, http_call_token2); + root_context->setNextHttpCallTokenForTesting(0); // Rollover. + EXPECT_EQ(root_context->nextHttpCallToken(), 1); + + EXPECT_EQ(root_context->getBuffer(WasmBufferType::HttpCallResponseBody), nullptr); + EXPECT_EQ(root_context->getBuffer(WasmBufferType::PluginConfiguration), nullptr); + + delete root_context; + + WasmStatePrototype wasm_state_prototype(true, WasmType::Bytes, "", + StreamInfo::FilterState::LifeSpan::FilterChain); + auto wasm_state = std::make_unique(wasm_state_prototype); + Protobuf::Arena arena; + EXPECT_EQ(wasm_state->exprValue(&arena, true).MessageOrDie(), nullptr); + wasm_state->setValue("foo"); + auto any = wasm_state->serializeAsProto(); + EXPECT_TRUE(static_cast(any.get())->Is()); +} + +TEST_P(WasmCommonTest, Logging) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "logging"; + auto plugin_configuration = "configure-test"; + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_shared( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + EXPECT_NE(wasm->buildVersion(), ""); + EXPECT_NE(std::unique_ptr(wasm->createContext(plugin)), nullptr); + wasm->setCreateContextForTesting( + [](Wasm*, const std::shared_ptr&) -> ContextBase* { return nullptr; }, + [](Wasm*, const std::shared_ptr&) -> ContextBase* { return nullptr; }); + EXPECT_EQ(std::unique_ptr(wasm->createContext(plugin)), nullptr); + auto wasm_weak = std::weak_ptr(wasm); + auto wasm_handle = std::make_shared(std::move(wasm)); + EXPECT_TRUE(wasm_weak.lock()->initialize(code, false)); + auto thread_local_wasm = std::make_shared(wasm_handle, *dispatcher); + thread_local_wasm.reset(); + + auto wasm_lock = wasm_weak.lock(); + wasm_lock->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, + log_(spdlog::level::info, Eq("on_configuration configure-test"))); + EXPECT_CALL(*root_context, log_(spdlog::level::trace, Eq("test trace logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::debug, Eq("test debug logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::warn, Eq("test warn logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::err, Eq("test error logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("log level is 1"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_done logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_delete logging"))); + return root_context; + }); + + auto root_context = static_cast(wasm_weak.lock()->start(plugin)); + EXPECT_EQ(root_context->getConfiguration(), "logging"); + if (GetParam() != "null") { + EXPECT_TRUE(root_context->validateConfiguration("", plugin)); + } + wasm_weak.lock()->configure(root_context, plugin); + EXPECT_EQ(root_context->getStatus().first, 0); + + wasm_handle.reset(); + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + // This will fault on nullptr if wasm has been deleted. + plugin->plugin_configuration_ = "done"; + wasm_weak.lock()->configure(root_context, plugin); + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + dispatcher->clearDeferredDeleteList(); +} + +TEST_P(WasmCommonTest, BadSignature) { + if (GetParam() != "v8") { + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = ""; + auto plugin_configuration = ""; + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/common/wasm/test_data/bad_signature_cpp.wasm")); + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_FALSE(wasm->initialize(code, false)); + EXPECT_TRUE(wasm->isFailed()); +} + +TEST_P(WasmCommonTest, Segv) { + if (GetParam() != "v8") { + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "segv"; + auto plugin_configuration = ""; + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm")); + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_TRUE(wasm->initialize(code, false)); + TestContext* root_context = nullptr; + wasm->setCreateContextForTesting( + nullptr, [&root_context](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::err, Eq("before badptr"))); + return root_context; + }); + wasm->start(plugin); + EXPECT_TRUE(wasm->isFailed()); + + // Subsequent calls should be NOOP(s). + + root_context->onResolveDns(0, Envoy::Network::DnsResolver::ResolutionStatus::Success, {}); + Envoy::Stats::MockMetricSnapshot stats_snapshot; + root_context->onStatsUpdate(stats_snapshot); +} + +TEST_P(WasmCommonTest, DivByZero) { + if (GetParam() != "v8") { + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "divbyzero"; + auto plugin_configuration = ""; + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm")); + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + auto context = std::make_unique(wasm.get()); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::err, Eq("before div by zero"))); + return root_context; + }); + wasm->start(plugin); +} + +TEST_P(WasmCommonTest, EmscriptenVersion) { + if (GetParam() != "v8") { + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = ""; + auto plugin_configuration = ""; + const auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm")); + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + auto context = std::make_unique(wasm.get()); + EXPECT_TRUE(wasm->initialize(code, false)); + + uint32_t major = 9, minor = 9, abi_major = 9, abi_minor = 9; + EXPECT_TRUE(wasm->getEmscriptenVersion(&major, &minor, &abi_major, &abi_minor)); + EXPECT_EQ(major, 0); + EXPECT_LE(minor, 3); + // Up to (at least) emsdk 1.39.6. + EXPECT_EQ(abi_major, 0); + EXPECT_LE(abi_minor, 20); +} + +TEST_P(WasmCommonTest, IntrinsicGlobals) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "globals"; + auto plugin_configuration = ""; + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::warn, Eq("NaN nan"))); + EXPECT_CALL(*root_context, log_(spdlog::level::warn, Eq("inf inf"))).Times(3); + return root_context; + }); + wasm->start(plugin); +} + +TEST_P(WasmCommonTest, Utilities) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "utilities"; + auto plugin_configuration = ""; + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_vm_start utilities"))); + return root_context; + }); + wasm->start(plugin); + + // Context + auto context = std::make_unique(); + context->error("error"); + + // Buffer + Extensions::Common::Wasm::Buffer buffer; + Extensions::Common::Wasm::Buffer const_buffer; + Extensions::Common::Wasm::Buffer string_buffer; + auto buffer_impl = std::make_unique("contents"); + buffer.set(buffer_impl.get()); + const_buffer.set(static_cast(buffer_impl.get())); + string_buffer.set("contents"); + std::string data("contents"); + if (GetParam() != "null") { + EXPECT_EQ(WasmResult::InvalidMemoryAccess, + buffer.copyTo(wasm.get(), 0, 1 << 30 /* length too long */, 0, 0)); + EXPECT_EQ(WasmResult::InvalidMemoryAccess, + buffer.copyTo(wasm.get(), 0, 1, 1 << 30 /* bad pointer location */, 0)); + EXPECT_EQ(WasmResult::InvalidMemoryAccess, + buffer.copyTo(wasm.get(), 0, 1, 0, 1 << 30 /* bad size location */)); + EXPECT_EQ(WasmResult::BadArgument, buffer.copyFrom(0, 1, data)); + EXPECT_EQ(WasmResult::BadArgument, buffer.copyFrom(1, 1, data)); + EXPECT_EQ(WasmResult::BadArgument, const_buffer.copyFrom(1, 1, data)); + EXPECT_EQ(WasmResult::BadArgument, string_buffer.copyFrom(1, 1, data)); + } +} + +TEST_P(WasmCommonTest, Stats) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "stats"; + auto plugin_configuration = ""; + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto vm_key = proxy_wasm::makeVmKey(vm_id, vm_configuration, code); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::trace, Eq("get counter = 1"))); + EXPECT_CALL(*root_context, log_(spdlog::level::debug, Eq("get counter = 2"))); + // recordMetric on a Counter is the same as increment. + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("get counter = 5"))); + EXPECT_CALL(*root_context, log_(spdlog::level::warn, Eq("get gauge = 2"))); + // Get is not supported on histograms. + EXPECT_CALL(*root_context, log_(spdlog::level::err, Eq("get histogram = Unsupported"))); + return root_context; + }); + wasm->start(plugin); +} + +TEST_P(WasmCommonTest, Foreign) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "foreign"; + auto vm_key = ""; + auto plugin_configuration = ""; + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::trace, Eq("compress 2000 -> 23"))); + EXPECT_CALL(*root_context, log_(spdlog::level::debug, Eq("uncompress 23 -> 2000"))); + return root_context; + }); + wasm->start(plugin); +} + +TEST_P(WasmCommonTest, OnForeign) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "on_foreign"; + auto vm_key = ""; + auto plugin_configuration = ""; + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm->initialize(code, false)); + TestContext* test_context = nullptr; + wasm->setCreateContextForTesting( + nullptr, [&test_context](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto context = new TestContext(wasm, plugin); + EXPECT_CALL(*context, log_(spdlog::level::debug, Eq("on_foreign start"))); + EXPECT_CALL(*context, log_(spdlog::level::info, Eq("on_foreign_function 7 13"))); + test_context = context; + return context; + }); + wasm->start(plugin); + test_context->onForeignFunction(7, 13); +} + +TEST_P(WasmCommonTest, WASI) { + if (GetParam() == "null") { + // This test has no meaning unless it is invoked by actual Wasm code + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "WASI"; + auto vm_key = ""; + auto plugin_configuration = ""; + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("WASI write to stdout"))).Times(1); + EXPECT_CALL(*root_context, log_(spdlog::level::err, Eq("WASI write to stderr"))).Times(1); + return root_context; + }); + wasm->start(plugin); +} + +TEST_P(WasmCommonTest, VmCache) { + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + NiceMock cluster_manager; + NiceMock init_manager; + NiceMock lifecycle_notifier; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider; + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "vm_cache"; + auto plugin_configuration = "init"; + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + + ServerLifecycleNotifier::StageCallbackWithCompletion lifecycle_callback; + EXPECT_CALL(lifecycle_notifier, registerCallback2(_, _)) + .WillRepeatedly( + Invoke([&](ServerLifecycleNotifier::Stage, + StageCallbackWithCompletion callback) -> ServerLifecycleNotifier::HandlePtr { + lifecycle_callback = callback; + return nullptr; + })); + + VmConfig vm_config; + vm_config.set_runtime(absl::StrCat("envoy.wasm.runtime.", GetParam())); + ProtobufWkt::StringValue vm_configuration_string; + vm_configuration_string.set_value(vm_configuration); + vm_config.mutable_configuration()->PackFrom(vm_configuration_string); + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + vm_config.mutable_code()->mutable_local()->set_inline_bytes(code); + WasmHandleSharedPtr wasm_handle; + createWasm(vm_config, plugin, scope, cluster_manager, init_manager, *dispatcher, *api, + lifecycle_notifier, remote_data_provider, + [&wasm_handle](const WasmHandleSharedPtr& w) { wasm_handle = w; }); + EXPECT_NE(wasm_handle, nullptr); + Event::PostCb post_cb = [] {}; + lifecycle_callback(post_cb); + + WasmHandleSharedPtr wasm_handle2; + createWasm(vm_config, plugin, scope, cluster_manager, init_manager, *dispatcher, *api, + lifecycle_notifier, remote_data_provider, + [&wasm_handle2](const WasmHandleSharedPtr& w) { wasm_handle2 = w; }); + EXPECT_NE(wasm_handle2, nullptr); + EXPECT_EQ(wasm_handle, wasm_handle2); + + auto wasm_handle_local = getOrCreateThreadLocalWasm( + wasm_handle, plugin, + [&dispatcher](const WasmHandleBaseSharedPtr& base_wasm) -> WasmHandleBaseSharedPtr { + auto wasm = + std::make_shared(std::static_pointer_cast(base_wasm), *dispatcher); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_vm_start vm_cache"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_configuration init"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_done logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_delete logging"))); + return root_context; + }); + return std::make_shared(wasm); + }); + wasm_handle.reset(); + wasm_handle2.reset(); + + auto wasm = wasm_handle_local->wasm().get(); + wasm_handle_local.reset(); + + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + + plugin->plugin_configuration_ = "done"; + wasm->configure(wasm->getContext(1), plugin); + plugin.reset(); + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + dispatcher->clearDeferredDeleteList(); + + proxy_wasm::clearWasmCachesForTesting(); +} + +TEST_P(WasmCommonTest, RemoteCode) { + if (GetParam() == "null") { + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + NiceMock cluster_manager; + NiceMock init_manager; + NiceMock lifecycle_notifier; + Init::ExpectableWatcherImpl init_watcher; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider; + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "vm_cache"; + auto plugin_configuration = "done"; + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + + std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + + VmConfig vm_config; + vm_config.set_runtime(absl::StrCat("envoy.wasm.runtime.", GetParam())); + ProtobufWkt::BytesValue vm_configuration_bytes; + vm_configuration_bytes.set_value(vm_configuration); + vm_config.mutable_configuration()->PackFrom(vm_configuration_bytes); + std::string sha256 = Extensions::Common::Wasm::sha256(code); + std::string sha256Hex = + Hex::encode(reinterpret_cast(&*sha256.begin()), sha256.size()); + vm_config.mutable_code()->mutable_remote()->set_sha256(sha256Hex); + vm_config.mutable_code()->mutable_remote()->mutable_http_uri()->set_uri( + "http://example.com/test.wasm"); + vm_config.mutable_code()->mutable_remote()->mutable_http_uri()->set_cluster("example_com"); + vm_config.mutable_code()->mutable_remote()->mutable_http_uri()->mutable_timeout()->set_seconds(5); + WasmHandleSharedPtr wasm_handle; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager, httpAsyncClientForCluster("example_com")) + .WillOnce(ReturnRef(cluster_manager.async_client_)); + EXPECT_CALL(cluster_manager.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return nullptr; + })); + + Init::TargetHandlePtr init_target_handle; + EXPECT_CALL(init_manager, add(_)).WillOnce(Invoke([&](const Init::Target& target) { + init_target_handle = target.createHandle("test"); + })); + createWasm(vm_config, plugin, scope, cluster_manager, init_manager, *dispatcher, *api, + lifecycle_notifier, remote_data_provider, + [&wasm_handle](const WasmHandleSharedPtr& w) { wasm_handle = w; }); + + EXPECT_CALL(init_watcher, ready()); + init_target_handle->initialize(init_watcher); + + EXPECT_NE(wasm_handle, nullptr); + + auto wasm_handle_local = getOrCreateThreadLocalWasm( + wasm_handle, plugin, + [&dispatcher](const WasmHandleBaseSharedPtr& base_wasm) -> WasmHandleBaseSharedPtr { + auto wasm = + std::make_shared(std::static_pointer_cast(base_wasm), *dispatcher); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_vm_start vm_cache"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_done logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_delete logging"))); + return root_context; + }); + return std::make_shared(wasm); + }); + wasm_handle.reset(); + + auto wasm = wasm_handle_local->wasm().get(); + wasm_handle_local.reset(); + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + wasm->configure(wasm->getContext(1), plugin); + plugin.reset(); + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + dispatcher->clearDeferredDeleteList(); +} + +TEST_P(WasmCommonTest, RemoteCodeMultipleRetry) { + if (GetParam() == "null") { + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + NiceMock cluster_manager; + NiceMock init_manager; + NiceMock lifecycle_notifier; + Init::ExpectableWatcherImpl init_watcher; + Event::DispatcherPtr dispatcher(api->allocateDispatcher("wasm_test")); + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider; + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "vm_cache"; + auto plugin_configuration = "done"; + auto plugin = std::make_shared( + name, root_id, vm_id, GetParam(), plugin_configuration, false, + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, nullptr); + + std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + + VmConfig vm_config; + vm_config.set_runtime(absl::StrCat("envoy.wasm.runtime.", GetParam())); + ProtobufWkt::StringValue vm_configuration_string; + vm_configuration_string.set_value(vm_configuration); + vm_config.mutable_configuration()->PackFrom(vm_configuration_string); + std::string sha256 = Extensions::Common::Wasm::sha256(code); + std::string sha256Hex = + Hex::encode(reinterpret_cast(&*sha256.begin()), sha256.size()); + int num_retries = 3; + vm_config.mutable_code()->mutable_remote()->set_sha256(sha256Hex); + vm_config.mutable_code()->mutable_remote()->mutable_http_uri()->set_uri( + "http://example.com/test.wasm"); + vm_config.mutable_code()->mutable_remote()->mutable_http_uri()->set_cluster("example_com"); + vm_config.mutable_code()->mutable_remote()->mutable_http_uri()->mutable_timeout()->set_seconds(5); + vm_config.mutable_code() + ->mutable_remote() + ->mutable_retry_policy() + ->mutable_num_retries() + ->set_value(num_retries); + WasmHandleSharedPtr wasm_handle; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager, httpAsyncClientForCluster("example_com")) + .WillRepeatedly(ReturnRef(cluster_manager.async_client_)); + EXPECT_CALL(cluster_manager.async_client_, send_(_, _, _)) + .WillRepeatedly(Invoke([&, retry = num_retries]( + Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) mutable + -> Http::AsyncClient::Request* { + if (retry-- == 0) { + Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "503"}}})); + callbacks.onSuccess(request, std::move(response)); + return nullptr; + } else { + Http::ResponseMessagePtr response(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return nullptr; + } + })); + + Init::TargetHandlePtr init_target_handle; + EXPECT_CALL(init_manager, add(_)).WillOnce(Invoke([&](const Init::Target& target) { + init_target_handle = target.createHandle("test"); + })); + createWasm(vm_config, plugin, scope, cluster_manager, init_manager, *dispatcher, *api, + lifecycle_notifier, remote_data_provider, + [&wasm_handle](const WasmHandleSharedPtr& w) { wasm_handle = w; }); + + EXPECT_CALL(init_watcher, ready()); + init_target_handle->initialize(init_watcher); + + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_NE(wasm_handle, nullptr); + + auto wasm_handle_local = getOrCreateThreadLocalWasm( + wasm_handle, plugin, + [&dispatcher](const WasmHandleBaseSharedPtr& base_wasm) -> WasmHandleBaseSharedPtr { + auto wasm = + std::make_shared(std::static_pointer_cast(base_wasm), *dispatcher); + wasm->setCreateContextForTesting( + nullptr, [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + auto root_context = new TestContext(wasm, plugin); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_vm_start vm_cache"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_done logging"))); + EXPECT_CALL(*root_context, log_(spdlog::level::info, Eq("on_delete logging"))); + return root_context; + }); + return std::make_shared(wasm); + }); + wasm_handle.reset(); + + auto wasm = wasm_handle_local->wasm().get(); + wasm_handle_local.reset(); + + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + wasm->configure(wasm->getContext(1), plugin); + plugin.reset(); + dispatcher->run(Event::Dispatcher::RunType::NonBlock); + dispatcher->clearDeferredDeleteList(); +} + +class WasmCommonContextTest + : public Common::Wasm::WasmTestBase> { +public: + WasmCommonContextTest() = default; + + void setup(const std::string& code, std::string vm_configuration, std::string root_id = "") { + setupBase( + GetParam(), code, + [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + return new TestContext(wasm, plugin); + }, + root_id, vm_configuration); + } + void setupContext() { + context_ = std::make_unique(wasm_->wasm().get(), root_context_->id(), plugin_); + context_->onCreate(); + } + + TestContext& rootContext() { return *static_cast(root_context_); } + TestContext& context() { return *context_; } + + std::unique_ptr context_; +}; + +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmCommonContextTest, test_values); + +TEST_P(WasmCommonContextTest, OnDnsResolve) { + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat( + "{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestContextCpp"; + } + EXPECT_FALSE(code.empty()); + + std::shared_ptr dns_resolver(new Network::MockDnsResolver()); + EXPECT_CALL(dispatcher_, createDnsResolver(_, _)).WillRepeatedly(Return(dns_resolver)); + Network::DnsResolver::ResolveCb dns_callback; + Network::MockActiveDnsQuery active_dns_query; + EXPECT_CALL(*dns_resolver, resolve(_, _, _)) + .WillRepeatedly( + testing::DoAll(testing::SaveArg<2>(&dns_callback), Return(&active_dns_query))); + + setup(code, "context"); + setupContext(); + EXPECT_CALL(rootContext(), log_(spdlog::level::warn, Eq("TestRootContext::onResolveDns 1"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::warn, Eq("TestRootContext::onResolveDns 2"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::info, + Eq("TestRootContext::onResolveDns dns 1001 192.168.1.101:0"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::info, + Eq("TestRootContext::onResolveDns dns 1001 192.168.1.102:0"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::warn, Eq("TestRootContext::onDone 1"))); + + dns_callback( + Network::DnsResolver::ResolutionStatus::Success, + TestUtility::makeDnsResponse({"192.168.1.101", "192.168.1.102"}, std::chrono::seconds(1001))); + + rootContext().onResolveDns(1 /* token */, Envoy::Network::DnsResolver::ResolutionStatus::Failure, + {}); + if (GetParam() == "null") { + rootContext().onTick(0); + } + if (GetParam() == "v8") { + rootContext().onQueueReady(0); + } + // Wait till the Wasm is destroyed and then the late callback should do nothing. + deferred_runner_.setFunction([dns_callback] { + dns_callback(Network::DnsResolver::ResolutionStatus::Success, + TestUtility::makeDnsResponse({"192.168.1.101", "192.168.1.102"}, + std::chrono::seconds(1001))); + }); +} + +TEST_P(WasmCommonContextTest, EmptyContext) { + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat( + "{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestContextCpp"; + } + EXPECT_FALSE(code.empty()); + + setup(code, "context", "empty"); + setupContext(); + + root_context_->onResolveDns(0, Envoy::Network::DnsResolver::ResolutionStatus::Success, {}); + NiceMock stats_snapshot; + root_context_->onStatsUpdate(stats_snapshot); + root_context_->validateConfiguration("", plugin_); +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/wasm/wasm_vm_test.cc b/test/extensions/common/wasm/wasm_vm_test.cc index b07b684a0ba4..e0775dfb0866 100644 --- a/test/extensions/common/wasm/wasm_vm_test.cc +++ b/test/extensions/common/wasm/wasm_vm_test.cc @@ -2,18 +2,21 @@ #include "common/stats/isolated_store_impl.h" -#include "extensions/common/wasm/null/null_vm_plugin.h" #include "extensions/common/wasm/wasm_vm.h" #include "test/test_common/environment.h" -#include "test/test_common/registry.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "include/proxy-wasm/null_vm_plugin.h" -using testing::HasSubstr; -using testing::Return; +using proxy_wasm::Cloneable; // NOLINT +using proxy_wasm::WasmCallVoid; // NOLINT +using proxy_wasm::WasmCallWord; // NOLINT +using proxy_wasm::Word; // NOLINT +using testing::HasSubstr; // NOLINT +using testing::Return; // NOLINT namespace Envoy { namespace Extensions { @@ -21,7 +24,7 @@ namespace Common { namespace Wasm { namespace { -class TestNullVmPlugin : public Null::NullVmPlugin { +class TestNullVmPlugin : public proxy_wasm::NullVmPlugin { public: TestNullVmPlugin() = default; ~TestNullVmPlugin() override = default; @@ -29,53 +32,41 @@ class TestNullVmPlugin : public Null::NullVmPlugin { MOCK_METHOD(void, start, ()); }; -class PluginFactory : public Null::NullVmPluginFactory { -public: - PluginFactory() = default; - - std::string name() const override { return "test_null_vm_plugin"; } - std::unique_ptr create() const override; -}; - TestNullVmPlugin* test_null_vm_plugin_ = nullptr; -std::unique_ptr PluginFactory::create() const { - auto result = std::make_unique(); - test_null_vm_plugin_ = result.get(); - return result; -} +proxy_wasm::RegisterNullVmPluginFactory register_test_null_vm_plugin("test_null_vm_plugin", []() { + auto plugin = std::make_unique(); + test_null_vm_plugin_ = plugin.get(); + return plugin; +}); class BaseVmTest : public testing::Test { public: - BaseVmTest() - : registration_(factory_), scope_(Stats::ScopeSharedPtr(stats_store.createScope("wasm."))) {} + BaseVmTest() : scope_(Stats::ScopeSharedPtr(stats_store.createScope("wasm."))) {} protected: - PluginFactory factory_; - Envoy::Registry::InjectFactory registration_; Stats::IsolatedStoreImpl stats_store; Stats::ScopeSharedPtr scope_; }; -TEST_F(BaseVmTest, NoRuntime) { - EXPECT_THROW_WITH_MESSAGE(createWasmVm("", scope_), WasmVmException, - "Failed to create WASM VM with unspecified runtime."); -} +TEST_F(BaseVmTest, NoRuntime) { EXPECT_EQ(createWasmVm("", scope_), nullptr); } TEST_F(BaseVmTest, BadRuntime) { - EXPECT_THROW_WITH_MESSAGE(createWasmVm("envoy.wasm.runtime.invalid", scope_), WasmVmException, - "Failed to create WASM VM using envoy.wasm.runtime.invalid runtime. " - "Envoy was compiled without support for it."); + EXPECT_EQ(createWasmVm("envoy.wasm.runtime.invalid", scope_), nullptr); } TEST_F(BaseVmTest, NullVmStartup) { auto wasm_vm = createWasmVm("envoy.wasm.runtime.null", scope_); EXPECT_TRUE(wasm_vm != nullptr); - EXPECT_TRUE(wasm_vm->runtime() == "envoy.wasm.runtime.null"); + EXPECT_TRUE(wasm_vm->runtime() == "null"); EXPECT_TRUE(wasm_vm->cloneable() == Cloneable::InstantiatedModule); auto wasm_vm_clone = wasm_vm->clone(); EXPECT_TRUE(wasm_vm_clone != nullptr); EXPECT_TRUE(wasm_vm->getCustomSection("user").empty()); + EXPECT_EQ(getEnvoyWasmIntegration(*wasm_vm).runtime(), "envoy.wasm.runtime.null"); + std::function f; + EXPECT_FALSE( + getEnvoyWasmIntegration(*wasm_vm).getNullVmFunction("bad_function", false, 0, nullptr, &f)); } TEST_F(BaseVmTest, NullVmMemory) { @@ -113,6 +104,7 @@ class MockHostFunctions { MOCK_METHOD(uint32_t, random, (), (const)); }; +#if defined(ENVOY_WASM_V8) MockHostFunctions* g_host_functions; void pong(void*, Word value) { g_host_functions->pong(convertWordToUint32(value)); } @@ -132,7 +124,9 @@ class WasmVmTest : public testing::TestWithParam { public: WasmVmTest() : scope_(Stats::ScopeSharedPtr(stats_store.createScope("wasm."))) {} - void SetUp() override { g_host_functions = new MockHostFunctions(); } + void SetUp() override { // NOLINT(readability-identifier-naming) + g_host_functions = new MockHostFunctions(); + } void TearDown() override { delete g_host_functions; } protected: @@ -159,7 +153,7 @@ TEST_P(WasmVmTest, V8Code) { #endif auto wasm_vm = createWasmVm("envoy.wasm.runtime.v8", scope_); ASSERT_TRUE(wasm_vm != nullptr); - EXPECT_TRUE(wasm_vm->runtime() == "envoy.wasm.runtime.v8"); + EXPECT_TRUE(wasm_vm->runtime() == "v8"); auto code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( "{{ test_rundir }}/test/extensions/common/wasm/test_data/test_rust.wasm")); @@ -192,23 +186,16 @@ TEST_P(WasmVmTest, V8BadHostFunctions) { EXPECT_TRUE(wasm_vm->load(code, GetParam())); wasm_vm->registerCallback("env", "random", &random, CONVERT_FUNCTION_WORD_TO_UINT32(random)); - EXPECT_THROW_WITH_MESSAGE(wasm_vm->link("test"), WasmVmException, - "Failed to load WASM module due to a missing import: env.pong"); + EXPECT_FALSE(wasm_vm->link("test")); wasm_vm->registerCallback("env", "pong", &bad_pong1, CONVERT_FUNCTION_WORD_TO_UINT32(bad_pong1)); - EXPECT_THROW_WITH_MESSAGE(wasm_vm->link("test"), WasmVmException, - "Failed to load WASM module due to an import type mismatch: env.pong, " - "want: i32 -> void, but host exports: void -> void"); + EXPECT_FALSE(wasm_vm->link("test")); wasm_vm->registerCallback("env", "pong", &bad_pong2, CONVERT_FUNCTION_WORD_TO_UINT32(bad_pong2)); - EXPECT_THROW_WITH_MESSAGE(wasm_vm->link("test"), WasmVmException, - "Failed to load WASM module due to an import type mismatch: env.pong, " - "want: i32 -> void, but host exports: i32 -> i32"); + EXPECT_FALSE(wasm_vm->link("test")); wasm_vm->registerCallback("env", "pong", &bad_pong3, CONVERT_FUNCTION_WORD_TO_UINT32(bad_pong3)); - EXPECT_THROW_WITH_MESSAGE(wasm_vm->link("test"), WasmVmException, - "Failed to load WASM module due to an import type mismatch: env.pong, " - "want: i32 -> void, but host exports: f64 -> f64"); + EXPECT_FALSE(wasm_vm->link("test")); } TEST_P(WasmVmTest, V8BadModuleFunctions) { @@ -239,11 +226,11 @@ TEST_P(WasmVmTest, V8BadModuleFunctions) { wasm_vm->getFunction("nonexistent", &sum); EXPECT_TRUE(sum == nullptr); - EXPECT_THROW_WITH_MESSAGE(wasm_vm->getFunction("ping", &sum), WasmVmException, - "Bad function signature for: ping"); + wasm_vm->getFunction("ping", &sum); + EXPECT_TRUE(wasm_vm->isFailed()); - EXPECT_THROW_WITH_MESSAGE(wasm_vm->getFunction("sum", &ping), WasmVmException, - "Bad function signature for: sum"); + wasm_vm->getFunction("sum", &ping); + EXPECT_TRUE(wasm_vm->isFailed()); } TEST_P(WasmVmTest, V8FunctionCalls) { @@ -282,13 +269,13 @@ TEST_P(WasmVmTest, V8FunctionCalls) { WasmCallWord<2> div; wasm_vm->getFunction("div", &div); - EXPECT_THROW_WITH_MESSAGE(div(nullptr /* no context */, 42, 0), WasmException, - "Function: div failed: Uncaught RuntimeError: unreachable"); + div(nullptr /* no context */, 42, 0); + EXPECT_TRUE(wasm_vm->isFailed()); WasmCallVoid<0> abort; wasm_vm->getFunction("abort", &abort); - EXPECT_THROW_WITH_MESSAGE(abort(nullptr /* no context */), WasmException, - "Function: abort failed: Uncaught RuntimeError: unreachable"); + abort(nullptr /* no context */); + EXPECT_TRUE(wasm_vm->isFailed()); } TEST_P(WasmVmTest, V8Memory) { @@ -331,6 +318,7 @@ TEST_P(WasmVmTest, V8Memory) { EXPECT_FALSE(wasm_vm->setWord(1024 * 1024 /* out of bound */, 1)); EXPECT_FALSE(wasm_vm->getWord(1024 * 1024 /* out of bound */, &word)); } +#endif } // namespace } // namespace Wasm diff --git a/test/extensions/filters/http/wasm/BUILD b/test/extensions/filters/http/wasm/BUILD new file mode 100644 index 000000000000..c36e60799cf6 --- /dev/null +++ b/test/extensions/filters/http/wasm/BUILD @@ -0,0 +1,62 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "wasm_filter_test", + size = "enormous", # For WAVM without precompilation. TODO: add precompilation. + srcs = ["wasm_filter_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/filters/http/wasm/test_data:async_call_rust.wasm", + "//test/extensions/filters/http/wasm/test_data:body_rust.wasm", + "//test/extensions/filters/http/wasm/test_data:headers_rust.wasm", + "//test/extensions/filters/http/wasm/test_data:metadata_rust.wasm", + "//test/extensions/filters/http/wasm/test_data:shared_data_rust.wasm", + "//test/extensions/filters/http/wasm/test_data:shared_queue_rust.wasm", + "//test/extensions/filters/http/wasm/test_data:test_cpp.wasm", + ]), + extension_name = "envoy.filters.http.wasm", + deps = [ + "//source/common/http:message_lib", + "//source/extensions/filters/http/wasm:wasm_filter_lib", + "//test/extensions/filters/http/wasm/test_data:test_cpp_plugin", + "//test/mocks/network:connection_mocks", + "//test/mocks/router:router_mocks", + "//test/test_common:wasm_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + size = "enormous", # For WAVM without precompilation. TODO: add precompilation. + srcs = ["config_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/filters/http/wasm/test_data:test_cpp.wasm", + ]), + extension_name = "envoy.filters.http.wasm", + deps = [ + "//source/common/common:base64_lib", + "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", + "//source/common/http:message_lib", + "//source/extensions/common/crypto:utility_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/filters/http/wasm:config", + "//test/mocks/server:server_mocks", + "//test/test_common:environment_lib", + "@envoy_api//envoy/extensions/filters/http/wasm/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/wasm/config_test.cc b/test/extensions/filters/http/wasm/config_test.cc new file mode 100644 index 000000000000..6b41185f7913 --- /dev/null +++ b/test/extensions/filters/http/wasm/config_test.cc @@ -0,0 +1,833 @@ +#include + +#include "envoy/extensions/filters/http/wasm/v3/wasm.pb.validate.h" + +#include "common/common/base64.h" +#include "common/common/hex.h" +#include "common/crypto/utility.h" +#include "common/http/message_impl.h" +#include "common/stats/isolated_store_impl.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/http/wasm/config.h" + +#include "test/mocks/http/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/test_common/environment.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { + +using Common::Wasm::WasmException; + +namespace HttpFilters { +namespace Wasm { + +#if defined(ENVOY_WASM_V8) || defined(ENVOY_WASM_WAVM) +class WasmFilterConfigTest : public Event::TestUsingSimulatedTime, + public testing::TestWithParam { +protected: + WasmFilterConfigTest() : api_(Api::createApiForTest(stats_store_)) { + ON_CALL(context_, api()).WillByDefault(ReturnRef(*api_)); + ON_CALL(context_, scope()).WillByDefault(ReturnRef(stats_store_)); + ON_CALL(context_, listenerMetadata()).WillByDefault(ReturnRef(listener_metadata_)); + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager_)); + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + ON_CALL(context_, dispatcher()).WillByDefault(ReturnRef(dispatcher_)); + } + + void SetUp() override { Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(); } + + void initializeForRemote() { + retry_timer_ = new Event::MockTimer(); + + EXPECT_CALL(dispatcher_, createTimer_(_)).WillOnce(Invoke([this](Event::TimerCb timer_cb) { + retry_timer_cb_ = timer_cb; + return retry_timer_; + })); + } + + NiceMock context_; + Stats::IsolatedStoreImpl stats_store_; + 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_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8" +#endif +#if defined(ENVOY_WASM_V8) && defined(ENVOY_WASM_WAVM) + , +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm" +#endif +); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmFilterConfigTest, testing_values); + +TEST_P(WasmFilterConfigTest, JsonLoadFromFileWasm) { + const std::string json = TestEnvironment::substitute(absl::StrCat(R"EOF( + { + "config" : { + "vm_config": { + "runtime": "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(", + "configuration": { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "some configuration" + }, + "code": { + "local": { + "filename": "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm" + } + }, + }}} + )EOF")); + + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromJson(json, proto_config); + WasmFilterConfig factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromFileWasm) { + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: "some configuration" + code: + local: + filename: "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm" + )EOF")); + + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromFileWasmFailOpenOk) { + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + fail_open: true + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: "some configuration" + code: + local: + filename: "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm" + )EOF")); + + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); +} + +TEST_P(WasmFilterConfigTest, YamlLoadInlineWasm) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + EXPECT_FALSE(code.empty()); + const std::string yaml = absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: { inline_bytes: ")EOF", + Base64::encode(code.data(), code.size()), R"EOF(" } + )EOF"); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); +} + +TEST_P(WasmFilterConfigTest, YamlLoadInlineBadCode) { + const std::string yaml = absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: + inline_string: "bad code" + )EOF"); + + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteWasm) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteWasmFailOnUncachedThenSucceed) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + nack_on_code_cache_miss: true + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + + auto cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + + cb(filter_callback); + dispatcher_.clearDeferredDeleteList(); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteWasmFailCachedThenSucceed) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + nack_on_code_cache_miss: true + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillRepeatedly(ReturnRef(cluster_manager_.async_client_)); + + Http::AsyncClient::Callbacks* async_callbacks = nullptr; + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillRepeatedly( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + // Store the callback the first time through for delayed call. + if (!async_callbacks) { + async_callbacks = &callbacks; + } else { + // Subsequent send()s happen inline. + callbacks.onSuccess( + request, + Http::ResponseMessagePtr{new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "503"}}})}); + } + return &request; + })); + + // Case 1: fail and fetch in the background, got 503, cache failure. + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); + // Fail a second time because we are in-progress. + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); + async_callbacks->onSuccess( + request, Http::ResponseMessagePtr{new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "503"}}})}); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Case 2: fail immediately with negatively cached result. + Init::ManagerImpl init_manager2{"init_manager2"}; + Init::ExpectableWatcherImpl init_watcher2; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager2)); + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); + + EXPECT_CALL(init_watcher2, ready()); + init_manager2.initialize(init_watcher2); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Wait for negative cache to timeout. + ::Envoy::Extensions::Common::Wasm::setTimeOffsetForCodeCacheForTesting(std::chrono::seconds(10)); + + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillRepeatedly( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + // Case 3: fail and fetch in the background, got 200, cache success. + Init::ManagerImpl init_manager3{"init_manager3"}; + Init::ExpectableWatcherImpl init_watcher3; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager3)); + + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); + + EXPECT_CALL(init_watcher3, ready()); + init_manager3.initialize(init_watcher3); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Case 4: success from cache. + Init::ManagerImpl init_manager4{"init_manager4"}; + Init::ExpectableWatcherImpl init_watcher4; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager4)); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + + EXPECT_CALL(init_watcher4, ready()); + init_manager4.initialize(init_watcher4); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + + cb(filter_callback); + + // Wait for cache to timeout. + ::Envoy::Extensions::Common::Wasm::setTimeOffsetForCodeCacheForTesting( + std::chrono::seconds(10 + 24 * 3600)); + + // Case 5: flush the stale cache. + const std::string sha256_2 = sha256 + "new"; + const std::string yaml2 = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + nack_on_code_cache_miss: true + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256_2)); + + envoy::extensions::filters::http::wasm::v3::Wasm proto_config2; + TestUtility::loadFromYaml(yaml2, proto_config2); + + Init::ManagerImpl init_manager5{"init_manager4"}; + Init::ExpectableWatcherImpl init_watcher5; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager5)); + + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config2, "stats", context_), + WasmException, "Unable to create Wasm HTTP filter "); + + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Case 6: fail and fetch in the background, got 200, cache success. + Init::ManagerImpl init_manager6{"init_manager6"}; + Init::ExpectableWatcherImpl init_watcher6; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager6)); + + factory.createFilterFactoryFromProto(proto_config, "stats", context_); + + EXPECT_CALL(init_watcher6, ready()); + init_manager6.initialize(init_watcher6); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + // Case 7: success from cache. + Init::ManagerImpl init_manager7{"init_manager7"}; + Init::ExpectableWatcherImpl init_watcher7; + + EXPECT_CALL(context_, initManager()).WillRepeatedly(ReturnRef(init_manager7)); + + Http::FilterFactoryCb cb2 = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + + EXPECT_CALL(init_watcher7, ready()); + init_manager7.initialize(init_watcher7); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + + Http::MockFilterChainFactoryCallbacks filter_callback2; + EXPECT_CALL(filter_callback2, addStreamFilter(_)); + EXPECT_CALL(filter_callback2, addAccessLogHandler(_)); + + cb2(filter_callback2); + + dispatcher_.clearDeferredDeleteList(); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteConnectionReset) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callbacks.onFailure(request, Envoy::Http::AsyncClient::FailureReason::Reset); + return &request; + })); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessWith503) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callbacks.onSuccess( + request, + Http::ResponseMessagePtr{new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "503"}}})}); + return &request; + })); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessIncorrectSha256) { + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 0 + sha256: xxxx )EOF")); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteMultipleRetries) { + initializeForRemote(); + const std::string code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + retry_policy: + num_retries: 3 + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + int num_retries = 3; + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillRepeatedly(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .Times(num_retries) + .WillRepeatedly( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "503"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + + EXPECT_CALL(*retry_timer_, enableTimer(_, _)) + .WillRepeatedly(Invoke([&](const std::chrono::milliseconds&, const ScopeTrackedObject*) { + if (--num_retries == 0) { + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce(Invoke( + [&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return &request; + })); + } + + retry_timer_cb_(); + })); + EXPECT_CALL(*retry_timer_, disableTimer()); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessBadcode) { + const std::string code = "foo"; + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return nullptr; + })); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + + // Fail closed. + Http::MockFilterChainFactoryCallbacks filter_callback; + Extensions::Common::Wasm::ContextSharedPtr context; + EXPECT_CALL(filter_callback, addStreamFilter(_)) + .WillOnce(Invoke([&context](Http::StreamFilterSharedPtr filter) { + context = std::static_pointer_cast(filter); + })); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + cb(filter_callback); + EXPECT_EQ(context->wasm(), nullptr); + EXPECT_TRUE(context->isFailed()); + + Http::MockStreamDecoderFilterCallbacks decoder_callbacks; + NiceMock stream_info; + + context->setDecoderFilterCallbacks(decoder_callbacks); + EXPECT_CALL(decoder_callbacks, streamInfo()).WillRepeatedly(ReturnRef(stream_info)); + EXPECT_CALL(stream_info, setResponseCodeDetails("wasm_fail_stream")); + EXPECT_CALL(decoder_callbacks, resetStream()); + + EXPECT_EQ(context->onRequestHeaders(10, false), proxy_wasm::FilterHeadersStatus::StopIteration); +} + +TEST_P(WasmFilterConfigTest, YamlLoadFromRemoteSuccessBadcodeFailOpen) { + const std::string code = "foo"; + const std::string sha256 = Hex::encode( + Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(Buffer::OwnedImpl(code))); + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + fail_open: true + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + remote: + http_uri: + uri: https://example.com/data + cluster: cluster_1 + timeout: 5s + sha256: )EOF", + sha256)); + envoy::extensions::filters::http::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + NiceMock client; + NiceMock request(&client); + + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster_1")) + .WillOnce(ReturnRef(cluster_manager_.async_client_)); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + Http::ResponseMessagePtr response( + new Http::ResponseMessageImpl(Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response->body().add(code); + callbacks.onSuccess(request, std::move(response)); + return nullptr; + })); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + Http::MockFilterChainFactoryCallbacks filter_callback; + // The filter is not registered. + cb(filter_callback); +} +#endif + +} // namespace Wasm +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/wasm/test_data/BUILD b/test/extensions/filters/http/wasm/test_data/BUILD new file mode 100644 index 000000000000..81741d0bb37c --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/BUILD @@ -0,0 +1,145 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary", "wasm_rust_binary") + +licenses(["notice"]) # Apache 2 + +envoy_package() + +wasm_rust_binary( + name = "async_call_rust.wasm", + srcs = ["async_call_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +wasm_rust_binary( + name = "body_rust.wasm", + srcs = ["body_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +wasm_rust_binary( + name = "headers_rust.wasm", + srcs = ["headers_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +wasm_rust_binary( + name = "metadata_rust.wasm", + srcs = ["metadata_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +wasm_rust_binary( + name = "shared_data_rust.wasm", + srcs = ["shared_data_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +wasm_rust_binary( + name = "shared_queue_rust.wasm", + srcs = ["shared_queue_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +envoy_cc_library( + name = "test_cpp_plugin", + srcs = [ + "test_async_call_cpp.cc", + "test_body_cpp.cc", + "test_cpp.cc", + "test_cpp_null_plugin.cc", + "test_grpc_call_cpp.cc", + "test_grpc_stream_cpp.cc", + "test_shared_data_cpp.cc", + "test_shared_queue_cpp.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + ":test_cc_proto", + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + "//source/extensions/common/wasm/ext:envoy_null_plugin", + "@proxy_wasm_cpp_sdk//contrib:contrib_lib", + ], +) + +envoy_wasm_cc_binary( + name = "test_cpp.wasm", + srcs = [ + "test_async_call_cpp.cc", + "test_body_cpp.cc", + "test_cpp.cc", + "test_grpc_call_cpp.cc", + "test_grpc_stream_cpp.cc", + "test_shared_data_cpp.cc", + "test_shared_queue_cpp.cc", + ], + deps = [ + ":test_cc_proto", + "//source/extensions/common/wasm/ext:declare_property_cc_proto", + "//source/extensions/common/wasm/ext:envoy_proxy_wasm_api_lib", + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics_lite", + "@proxy_wasm_cpp_sdk//contrib:contrib_lib", + ], +) + +# NB: this target is compiled both to native code and to Wasm. Hence the generic rule. +proto_library( + name = "test_proto", + srcs = ["test.proto"], + deps = [ + "@com_google_protobuf//:any_proto", + "@com_google_protobuf//:timestamp_proto", + ], +) + +# NB: this target is compiled both to native code and to Wasm. Hence the generic rule. +cc_proto_library( + name = "test_cc_proto", + deps = [":test_proto"], +) + +# TODO: FIXME +# +#filegroup( +# name = "wavm_binary", +# srcs = ["//bazel/foreign_cc:wavm"], +# output_group = "wavm", +#) +# +#genrule( +# name = "test_cpp_wavm_compile", +# srcs = [":test_cpp.wasm"], +# outs = ["test_cpp.wavm_compiled.wasm"], +# cmd = "./$(location wavm_binary) compile $(location test_cpp.wasm) $(location test_cpp.wavm_compiled.wasm)", +# tools = [ +# ":test_cpp.wasm", +# ":wavm_binary", +# ], +#) diff --git a/test/extensions/filters/http/wasm/test_data/async_call_rust/Cargo.toml b/test/extensions/filters/http/wasm/test_data/async_call_rust/Cargo.toml new file mode 100644 index 000000000000..b5c503a6356c --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/async_call_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm async call test" +name = "async_call_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/http/wasm/test_data/async_call_rust/src/lib.rs b/test/extensions/filters/http/wasm/test_data/async_call_rust/src/lib.rs new file mode 100644 index 000000000000..0cb7833c4437 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/async_call_rust/src/lib.rs @@ -0,0 +1,45 @@ +use log::{debug, info, warn}; +use proxy_wasm::traits::{Context, HttpContext}; +use proxy_wasm::types::*; +use std::time::Duration; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_http_context(|_, _| -> Box { Box::new(TestStream) }); +} + +struct TestStream; + +impl HttpContext for TestStream { + fn on_http_request_headers(&mut self, _: usize) -> Action { + self.dispatch_http_call( + "cluster", + vec![(":method", "POST"), (":path", "/"), (":authority", "foo")], + Some(b"hello world"), + vec![("trail", "cow")], + Duration::from_secs(5), + ) + .unwrap(); + info!("onRequestHeaders"); + Action::Pause + } +} + +impl Context for TestStream { + fn on_http_call_response(&mut self, _: u32, _: usize, body_size: usize, _: usize) { + if body_size == 0 { + info!("async_call failed"); + return; + } + for (name, value) in &self.get_http_call_response_headers() { + info!("{} -> {}", name, value); + } + if let Some(body) = self.get_http_call_response_body(0, body_size) { + debug!("{}", String::from_utf8(body).unwrap()); + } + for (name, value) in &self.get_http_call_response_trailers() { + warn!("{} -> {}", name, value); + } + } +} diff --git a/test/extensions/filters/http/wasm/test_data/body_rust/Cargo.toml b/test/extensions/filters/http/wasm/test_data/body_rust/Cargo.toml new file mode 100644 index 000000000000..bded4bb904f9 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/body_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm HTTP body test" +name = "body_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/http/wasm/test_data/body_rust/src/lib.rs b/test/extensions/filters/http/wasm/test_data/body_rust/src/lib.rs new file mode 100644 index 000000000000..4c577e64695f --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/body_rust/src/lib.rs @@ -0,0 +1,208 @@ +use log::error; +use proxy_wasm::traits::{Context, HttpContext}; +use proxy_wasm::types::*; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_http_context(|_, _| -> Box { + Box::new(TestStream { + test: None, + body_chunks: 0, + }) + }); +} + +struct TestStream { + test: Option, + body_chunks: usize, +} + +impl HttpContext for TestStream { + fn on_http_request_headers(&mut self, _: usize) -> Action { + self.test = self.get_http_request_header("x-test-operation"); + self.body_chunks = 0; + Action::Continue + } + + fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + match self.test.as_deref() { + Some("ReadBody") => { + let body = self.get_http_request_body(0, body_size).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + Action::Continue + } + Some("PrependAndAppendToBody") => { + self.set_http_request_body(0, 0, b"prepend."); + self.set_http_request_body(0xffffffff, 0, b".append"); + let body = self.get_http_request_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + Action::Continue + } + Some("ReplaceBody") => { + self.set_http_request_body(0, 0xffffffff, b"replace"); + let body = self.get_http_request_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + Action::Continue + } + Some("RemoveBody") => { + self.set_http_request_body(0, 0xffffffff, b""); + if let Some(body) = self.get_http_request_body(0, 0xffffffff) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } else { + error!("onBody "); + } + Action::Continue + } + Some("BufferBody") => { + let body = self.get_http_request_body(0, body_size).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("PrependAndAppendToBufferedBody") => { + self.set_http_request_body(0, 0, b"prepend."); + self.set_http_request_body(0xffffffff, 0, b".append"); + let body = self.get_http_request_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("ReplaceBufferedBody") => { + self.set_http_request_body(0, 0xffffffff, b"replace"); + let body = self.get_http_request_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("RemoveBufferedBody") => { + self.set_http_request_body(0, 0xffffffff, b""); + if let Some(body) = self.get_http_request_body(0, 0xffffffff) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } else { + error!("onBody "); + } + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("BufferTwoBodies") => { + if let Some(body) = self.get_http_request_body(0, body_size) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } + self.body_chunks += 1; + if end_of_stream || self.body_chunks > 2 { + Action::Continue + } else { + Action::Pause + } + } + _ => Action::Continue, + } + } + + fn on_http_response_headers(&mut self, _: usize) -> Action { + self.test = self.get_http_response_header("x-test-operation"); + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + match self.test.as_deref() { + Some("ReadBody") => { + let body = self.get_http_response_body(0, body_size).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + Action::Continue + } + Some("PrependAndAppendToBody") => { + self.set_http_response_body(0, 0, b"prepend."); + self.set_http_response_body(0xffffffff, 0, b".append"); + let body = self.get_http_response_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + Action::Continue + } + Some("ReplaceBody") => { + self.set_http_response_body(0, 0xffffffff, b"replace"); + let body = self.get_http_response_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + Action::Continue + } + Some("RemoveBody") => { + self.set_http_response_body(0, 0xffffffff, b""); + if let Some(body) = self.get_http_response_body(0, 0xffffffff) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } else { + error!("onBody "); + } + Action::Continue + } + Some("BufferBody") => { + let body = self.get_http_response_body(0, body_size).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("PrependAndAppendToBufferedBody") => { + self.set_http_response_body(0, 0, b"prepend."); + self.set_http_response_body(0xffffffff, 0, b".append"); + let body = self.get_http_response_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("ReplaceBufferedBody") => { + self.set_http_response_body(0, 0xffffffff, b"replace"); + let body = self.get_http_response_body(0, 0xffffffff).unwrap(); + error!("onBody {}", String::from_utf8(body).unwrap()); + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("RemoveBufferedBody") => { + self.set_http_response_body(0, 0xffffffff, b""); + if let Some(body) = self.get_http_response_body(0, 0xffffffff) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } else { + error!("onBody "); + } + if end_of_stream { + Action::Continue + } else { + Action::Pause + } + } + Some("BufferTwoBodies") => { + if let Some(body) = self.get_http_response_body(0, body_size) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } + self.body_chunks += 1; + if end_of_stream || self.body_chunks > 2 { + Action::Continue + } else { + Action::Pause + } + } + _ => Action::Continue, + } + } +} + +impl Context for TestStream {} diff --git a/test/extensions/filters/http/wasm/test_data/headers_rust/Cargo.toml b/test/extensions/filters/http/wasm/test_data/headers_rust/Cargo.toml new file mode 100644 index 000000000000..4d03b9e6358a --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/headers_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm HTTP headers test" +name = "headers_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/http/wasm/test_data/headers_rust/src/lib.rs b/test/extensions/filters/http/wasm/test_data/headers_rust/src/lib.rs new file mode 100644 index 000000000000..6d9fc94a7a9c --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/headers_rust/src/lib.rs @@ -0,0 +1,55 @@ +use log::{debug, error, info, warn}; +use proxy_wasm::traits::{Context, HttpContext}; +use proxy_wasm::types::*; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_http_context(|context_id, _| -> Box { + Box::new(TestStream { context_id }) + }); +} + +struct TestStream { + context_id: u32, +} + +impl HttpContext for TestStream { + fn on_http_request_headers(&mut self, _: usize) -> Action { + debug!("onRequestHeaders {} headers", self.context_id); + if let Some(path) = self.get_http_request_header(":path") { + info!("header path {}", path); + } + let action = match self.get_http_request_header("server").as_deref() { + Some("envoy-wasm-pause") => Action::Pause, + _ => Action::Continue, + }; + self.set_http_request_header("newheader", Some("newheadervalue")); + self.set_http_request_header("server", Some("envoy-wasm")); + action + } + + fn on_http_request_body(&mut self, body_size: usize, _: bool) -> Action { + if let Some(body) = self.get_http_request_body(0, body_size) { + error!("onBody {}", String::from_utf8(body).unwrap()); + } + Action::Continue + } + + fn on_http_response_trailers(&mut self, _: usize) -> Action { + Action::Pause + } + + fn on_log(&mut self) { + if let Some(path) = self.get_http_request_header(":path") { + warn!("onLog {} {}", self.context_id, path); + } + } +} + +impl Context for TestStream { + fn on_done(&mut self) -> bool { + warn!("onDone {}", self.context_id); + true + } +} diff --git a/test/extensions/filters/http/wasm/test_data/metadata_rust/Cargo.toml b/test/extensions/filters/http/wasm/test_data/metadata_rust/Cargo.toml new file mode 100644 index 000000000000..e070d6454be3 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/metadata_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm metadata test" +name = "metadata_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/http/wasm/test_data/metadata_rust/src/lib.rs b/test/extensions/filters/http/wasm/test_data/metadata_rust/src/lib.rs new file mode 100644 index 000000000000..3b708b2d3599 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/metadata_rust/src/lib.rs @@ -0,0 +1,93 @@ +use log::{debug, error, info, trace}; +use proxy_wasm::traits::{Context, HttpContext, RootContext}; +use proxy_wasm::types::*; +use std::convert::TryFrom; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(TestRoot) }); + proxy_wasm::set_http_context(|_, _| -> Box { Box::new(TestStream) }); +} + +struct TestRoot; + +impl Context for TestRoot {} + +impl RootContext for TestRoot { + fn on_tick(&mut self) { + if let Some(value) = self.get_property(vec!["node", "metadata", "wasm_node_get_key"]) { + debug!("onTick {}", String::from_utf8(value).unwrap()); + } else { + debug!("missing node metadata"); + } + } +} + +struct TestStream; + +impl Context for TestStream {} + +impl HttpContext for TestStream { + fn on_http_request_headers(&mut self, _: usize) -> Action { + if self + .get_property(vec!["node", "metadata", "wasm_node_get_key"]) + .is_none() + { + debug!("missing node metadata"); + } + + self.set_property( + vec!["wasm_request_set_key"], + Some(b"wasm_request_set_value"), + ); + + if let Some(path) = self.get_http_request_header(":path") { + info!("header path {}", path); + } + self.set_http_request_header("newheader", Some("newheadervalue")); + self.set_http_request_header("server", Some("envoy-wasm")); + + if let Some(value) = self.get_property(vec!["request", "duration"]) { + info!( + "duration is {}", + u64::from_le_bytes(<[u8; 8]>::try_from(&value[0..8]).unwrap()) + ); + } else { + error!("failed to get request duration"); + } + Action::Continue + } + + fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { + if let Some(value) = self.get_property(vec!["node", "metadata", "wasm_node_get_key"]) { + error!("onBody {}", String::from_utf8(value).unwrap()); + } else { + debug!("missing node metadata"); + } + let key1 = self.get_property(vec![ + "metadata", + "filter_metadata", + "envoy.filters.http.wasm", + "wasm_request_get_key", + ]); + if key1.is_none() { + debug!("missing request metadata"); + } + let key2 = self.get_property(vec![ + "metadata", + "filter_metadata", + "envoy.filters.http.wasm", + "wasm_request_get_key", + ]); + if key2.is_none() { + debug!("missing request metadata"); + } + trace!( + "Struct {} {}", + String::from_utf8(key1.unwrap()).unwrap(), + String::from_utf8(key2.unwrap()).unwrap() + ); + Action::Continue + } +} diff --git a/test/extensions/filters/http/wasm/test_data/shared_data_rust/Cargo.toml b/test/extensions/filters/http/wasm/test_data/shared_data_rust/Cargo.toml new file mode 100644 index 000000000000..795905cb03e7 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/shared_data_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm shared key-value store test" +name = "shared_data_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/http/wasm/test_data/shared_data_rust/src/lib.rs b/test/extensions/filters/http/wasm/test_data/shared_data_rust/src/lib.rs new file mode 100644 index 000000000000..8a19c684abc5 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/shared_data_rust/src/lib.rs @@ -0,0 +1,49 @@ +use log::{debug, info, warn}; +use proxy_wasm::traits::{Context, RootContext}; +use proxy_wasm::types::*; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(TestRoot) }); +} + +struct TestRoot; + +impl Context for TestRoot {} + +impl RootContext for TestRoot { + fn on_tick(&mut self) { + if self.get_shared_data("shared_data_key_bad") == (None, None) { + debug!("get of bad key not found"); + } + self.set_shared_data("shared_data_key1", Some(b"shared_data_value0"), None) + .unwrap(); + self.set_shared_data("shared_data_key1", Some(b"shared_data_value1"), None) + .unwrap(); + self.set_shared_data("shared_data_key2", Some(b"shared_data_value2"), None) + .unwrap(); + if let (_, Some(cas)) = self.get_shared_data("shared_data_key2") { + match self.set_shared_data( + "shared_data_key2", + Some(b"shared_data_value3"), + Some(cas + 1), + ) { + Err(Status::CasMismatch) => info!("set CasMismatch"), + _ => panic!(), + }; + } + } + + fn on_queue_ready(&mut self, _: u32) { + if self.get_shared_data("shared_data_key_bad") == (None, None) { + debug!("second get of bad key not found"); + } + if let (Some(value), _) = self.get_shared_data("shared_data_key1") { + debug!("get 1 {}", String::from_utf8(value).unwrap()); + } + if let (Some(value), _) = self.get_shared_data("shared_data_key2") { + warn!("get 2 {}", String::from_utf8(value).unwrap()); + } + } +} diff --git a/test/extensions/filters/http/wasm/test_data/shared_queue_rust/Cargo.toml b/test/extensions/filters/http/wasm/test_data/shared_queue_rust/Cargo.toml new file mode 100644 index 000000000000..0ba3e9070c32 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/shared_queue_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm shared queue test" +name = "shared_queue_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/http/wasm/test_data/shared_queue_rust/src/lib.rs b/test/extensions/filters/http/wasm/test_data/shared_queue_rust/src/lib.rs new file mode 100644 index 000000000000..1269f37f9cff --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/shared_queue_rust/src/lib.rs @@ -0,0 +1,61 @@ +use log::{debug, info, warn}; +use proxy_wasm::traits::{Context, HttpContext, RootContext}; +use proxy_wasm::types::*; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { + Box::new(TestRoot { queue_id: None }) + }); + proxy_wasm::set_http_context(|_, _| -> Box { Box::new(TestStream) }); +} + +struct TestRoot { + queue_id: Option, +} + +impl Context for TestRoot {} + +impl RootContext for TestRoot { + fn on_vm_start(&mut self, _: usize) -> bool { + self.queue_id = Some(self.register_shared_queue("my_shared_queue")); + true + } + + fn on_queue_ready(&mut self, queue_id: u32) { + if Some(queue_id) == self.queue_id { + info!("onQueueReady"); + match self.dequeue_shared_queue(9999999 /* bad queue_id */) { + Err(Status::NotFound) => warn!("onQueueReady bad token not found"), + _ => (), + } + if let Some(value) = self.dequeue_shared_queue(queue_id).unwrap() { + debug!("data {} Ok", String::from_utf8(value).unwrap()); + } + if self.dequeue_shared_queue(queue_id).unwrap().is_none() { + warn!("onQueueReady extra data not found"); + } + } + } +} + +struct TestStream; + +impl Context for TestStream {} + +impl HttpContext for TestStream { + fn on_http_request_headers(&mut self, _: usize) -> Action { + if self + .resolve_shared_queue("vm_id", "bad_shared_queue") + .is_none() + { + warn!("onRequestHeaders not found bad_shared_queue"); + } + if let Some(queue_id) = self.resolve_shared_queue("vm_id", "my_shared_queue") { + self.enqueue_shared_queue(queue_id, Some(b"data1")).unwrap(); + warn!("onRequestHeaders enqueue Ok"); + } + Action::Continue + } +} diff --git a/test/extensions/filters/http/wasm/test_data/test.proto b/test/extensions/filters/http/wasm/test_data/test.proto new file mode 100644 index 000000000000..1b055c7ca760 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package wasmtest; + +import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; + +message TestProto { + uint64 i = 1; + double j = 2; + bool k = 3; + string s = 4; + google.protobuf.Timestamp t = 5; + google.protobuf.Any a = 6; + TestProto b = 7; + repeated string l = 8; + map m = 9; +}; diff --git a/test/extensions/filters/http/wasm/test_data/test_async_call_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_async_call_cpp.cc new file mode 100644 index 000000000000..8075ef63b578 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_async_call_cpp.cc @@ -0,0 +1,76 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +class AsyncCallContext : public Context { +public: + explicit AsyncCallContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; +}; + +class AsyncCallRootContext : public RootContext { +public: + explicit AsyncCallRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {} +}; + +static RegisterContextFactory register_AsyncCallContext(CONTEXT_FACTORY(AsyncCallContext), + ROOT_FACTORY(AsyncCallRootContext), + "async_call"); + +FilterHeadersStatus AsyncCallContext::onRequestHeaders(uint32_t, bool end_of_stream) { + auto context_id = id(); + auto callback = [context_id](uint32_t, size_t body_size, uint32_t) { + if (body_size == 0) { + logInfo("async_call failed"); + return; + } + auto response_headers = getHeaderMapPairs(WasmHeaderMapType::HttpCallResponseHeaders); + // Switch context after getting headers, but before getting body to exercise both code paths. + getContext(context_id)->setEffectiveContext(); + auto body = getBufferBytes(WasmBufferType::HttpCallResponseBody, 0, body_size); + auto response_trailers = getHeaderMapPairs(WasmHeaderMapType::HttpCallResponseTrailers); + for (auto& p : response_headers->pairs()) { + logInfo(std::string(p.first) + std::string(" -> ") + std::string(p.second)); + } + logDebug(std::string(body->view())); + for (auto& p : response_trailers->pairs()) { + logWarn(std::string(p.first) + std::string(" -> ") + std::string(p.second)); + } + }; + if (end_of_stream) { + if (root()->httpCall("cluster", {{":method", "POST"}, {":path", "/"}, {":authority", "foo"}}, + "hello world", {{"trail", "cow"}}, 1000, callback) == WasmResult::Ok) { + logError("expected failure did not"); + } + return FilterHeadersStatus::Continue; + } + if (root()->httpCall("bogus cluster", + {{":method", "POST"}, {":path", "/"}, {":authority", "foo"}}, "hello world", + {{"trail", "cow"}}, 1000, callback) == WasmResult::Ok) { + logError("bogus cluster found error"); + } + if (root()->httpCall("cluster", {{":method", "POST"}, {":path", "/"}, {":authority", "foo"}}, + "hello world", {{"trail", "cow"}}, 0xFFFFFFFF, callback) == WasmResult::Ok) { + logError("bogus timeout accepted error"); + } + if (root()->httpCall("cluster", {{":method", "POST"}, {":authority", "foo"}}, "hello world", + {{"trail", "cow"}}, 1000, callback) == WasmResult::Ok) { + logError("emissing path accepted error"); + } + root()->httpCall("cluster", {{":method", "POST"}, {":path", "/"}, {":authority", "foo"}}, + "hello world", {{"trail", "cow"}}, 1000, callback); + logInfo("onRequestHeaders"); + return FilterHeadersStatus::StopIteration; +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/test_data/test_body_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_body_cpp.cc new file mode 100644 index 000000000000..27d0e0626ff4 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_body_cpp.cc @@ -0,0 +1,136 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +class BodyRootContext : public RootContext { +public: + explicit BodyRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {} +}; + +class BodyContext : public Context { +public: + explicit BodyContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; + FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override; + FilterHeadersStatus onResponseHeaders(uint32_t, bool) override; + FilterDataStatus onResponseBody(size_t body_buffer_length, bool end_of_stream) override; + +private: + BodyRootContext* root() { return static_cast(Context::root()); } + static void logBody(WasmBufferType type); + FilterDataStatus onBody(WasmBufferType type, size_t buffer_length, bool end); + std::string body_op_; + int num_chunks_ = 0; +}; + +static RegisterContextFactory register_BodyContext(CONTEXT_FACTORY(BodyContext), + ROOT_FACTORY(BodyRootContext), "body"); + +void BodyContext::logBody(WasmBufferType type) { + size_t buffered_size; + uint32_t flags; + getBufferStatus(type, &buffered_size, &flags); + auto body = getBufferBytes(type, 0, buffered_size); + logError(std::string("onBody ") + std::string(body->view())); +} + +FilterDataStatus BodyContext::onBody(WasmBufferType type, size_t buffer_length, + bool end_of_stream) { + size_t size; + uint32_t flags; + if (body_op_ == "ReadBody") { + auto body = getBufferBytes(type, 0, buffer_length); + logError("onBody " + std::string(body->view())); + + } else if (body_op_ == "PrependAndAppendToBody") { + setBuffer(WasmBufferType::HttpRequestBody, 0, 0, "prepend."); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + setBuffer(WasmBufferType::HttpRequestBody, size, 0, ".append"); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + auto updated = getBufferBytes(WasmBufferType::HttpRequestBody, 0, size); + logError("onBody " + std::string(updated->view())); + return FilterDataStatus::StopIterationNoBuffer; + } else if (body_op_ == "ReplaceBody") { + setBuffer(WasmBufferType::HttpRequestBody, 0, buffer_length, "replace"); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + auto replaced = getBufferBytes(WasmBufferType::HttpRequestBody, 0, size); + logError("onBody " + std::string(replaced->view())); + return FilterDataStatus::StopIterationAndWatermark; + } else if (body_op_ == "RemoveBody") { + setBuffer(WasmBufferType::HttpRequestBody, 0, buffer_length, ""); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + auto erased = getBufferBytes(WasmBufferType::HttpRequestBody, 0, size); + logError("onBody " + std::string(erased->view())); + + } else if (body_op_ == "BufferBody") { + logBody(type); + return end_of_stream ? FilterDataStatus::Continue : FilterDataStatus::StopIterationAndBuffer; + + } else if (body_op_ == "PrependAndAppendToBufferedBody") { + setBuffer(WasmBufferType::HttpRequestBody, 0, 0, "prepend."); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + setBuffer(WasmBufferType::HttpRequestBody, size, 0, ".append"); + logBody(type); + return end_of_stream ? FilterDataStatus::Continue : FilterDataStatus::StopIterationAndBuffer; + + } else if (body_op_ == "ReplaceBufferedBody") { + setBuffer(WasmBufferType::HttpRequestBody, 0, buffer_length, "replace"); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + auto replaced = getBufferBytes(WasmBufferType::HttpRequestBody, 0, size); + logBody(type); + return end_of_stream ? FilterDataStatus::Continue : FilterDataStatus::StopIterationAndBuffer; + + } else if (body_op_ == "RemoveBufferedBody") { + setBuffer(WasmBufferType::HttpRequestBody, 0, buffer_length, ""); + getBufferStatus(WasmBufferType::HttpRequestBody, &size, &flags); + auto erased = getBufferBytes(WasmBufferType::HttpRequestBody, 0, size); + logBody(type); + return end_of_stream ? FilterDataStatus::Continue : FilterDataStatus::StopIterationAndBuffer; + + } else if (body_op_ == "BufferTwoBodies") { + logBody(type); + num_chunks_++; + if (end_of_stream || num_chunks_ > 2) { + return FilterDataStatus::Continue; + } + return FilterDataStatus::StopIterationAndBuffer; + + } else { + // This is a test and the test was configured incorrectly. + logError("Invalid body test op " + body_op_); + abort(); + } + return FilterDataStatus::Continue; +} + +FilterHeadersStatus BodyContext::onRequestHeaders(uint32_t, bool) { + body_op_ = getRequestHeader("x-test-operation")->toString(); + setRequestHeaderPairs({{"a", "a"}, {"b", "b"}}); + return FilterHeadersStatus::Continue; +} + +FilterHeadersStatus BodyContext::onResponseHeaders(uint32_t, bool) { + body_op_ = getResponseHeader("x-test-operation")->toString(); + CHECK_RESULT(replaceResponseHeader("x-test-operation", body_op_)); + return FilterHeadersStatus::Continue; +} + +FilterDataStatus BodyContext::onRequestBody(size_t body_buffer_length, bool end_of_stream) { + return onBody(WasmBufferType::HttpRequestBody, body_buffer_length, end_of_stream); +} + +FilterDataStatus BodyContext::onResponseBody(size_t body_buffer_length, bool end_of_stream) { + return onBody(WasmBufferType::HttpResponseBody, body_buffer_length, end_of_stream); +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/test_data/test_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_cpp.cc new file mode 100644 index 000000000000..92361e299781 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_cpp.cc @@ -0,0 +1,734 @@ +// NOLINT(namespace-envoy) +#include +#include +#include +#include "test/extensions/filters/http/wasm/test_data/test.pb.h" + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#include "source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +#include "source/extensions/common/wasm/ext/declare_property.pb.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#include "absl/base/casts.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +#include "contrib/proxy_expr.h" + +class TestRootContext : public RootContext { +public: + explicit TestRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {} + + bool onStart(size_t configuration_size) override; + void onTick() override; + bool onConfigure(size_t) override; + + std::string test_; + uint32_t stream_context_id_; +}; + +class TestContext : public Context { +public: + explicit TestContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; + FilterTrailersStatus onRequestTrailers(uint32_t) override; + FilterTrailersStatus onResponseTrailers(uint32_t) override; + FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override; + void onLog() override; + void onDone() override; + +private: + TestRootContext* root() { return static_cast(Context::root()); } +}; + +static RegisterContextFactory register_TestContext(CONTEXT_FACTORY(TestContext), + ROOT_FACTORY(TestRootContext)); + +bool TestRootContext::onStart(size_t configuration_size) { + test_ = getBufferBytes(WasmBufferType::VmConfiguration, 0, configuration_size)->toString(); + return true; +} + +bool TestRootContext::onConfigure(size_t) { + if (test_ == "property") { + { + // Many properties are not available in the root context. + const std::vector properties = { + "string_state", "metadata", "request", "response", "connection", + "connection_id", "upstream", "source", "destination", "cluster_name", + "cluster_metadata", "route_name", "route_metadata", + }; + for (const auto& property : properties) { + if (getProperty({property}).has_value()) { + logWarn("getProperty should not return a value in the root context"); + } + } + } + { + // Some properties are defined in the root context. + std::vector, std::string>> properties = { + {{"plugin_name"}, "plugin_name"}, + {{"plugin_vm_id"}, "vm_id"}, + {{"listener_direction"}, std::string("\x1\0\0\0\0\0\0\0\0", 8)}, // INBOUND + {{"listener_metadata"}, ""}, + }; + for (const auto& property : properties) { + std::string value; + if (!getValue(property.first, &value)) { + logWarn("getValue should provide a value in the root context: " + property.second); + } + if (value != property.second) { + logWarn("getValue returned " + value + ", expect " + property.second); + } + } + } + } + return true; +} + +FilterHeadersStatus TestContext::onRequestHeaders(uint32_t, bool) { + root()->stream_context_id_ = id(); + auto test = root()->test_; + if (test == "headers") { + logDebug(std::string("onRequestHeaders ") + std::to_string(id()) + std::string(" ") + test); + auto path = getRequestHeader(":path"); + logInfo(std::string("header path ") + std::string(path->view())); + std::string protocol; + addRequestHeader("newheader", "newheadervalue"); + auto server = getRequestHeader("server"); + replaceRequestHeader("server", "envoy-wasm"); + auto r = addResponseHeader("bad", "bad"); + if (r != WasmResult::BadArgument) { + logWarn("unexpected success of addResponseHeader"); + } + if (addResponseTrailer("bad", "bad") != WasmResult::BadArgument) { + logWarn("unexpected success of addResponseTrailer"); + } + if (removeResponseTrailer("bad") != WasmResult::BadArgument) { + logWarn("unexpected success of remoteResponseTrailer"); + } + size_t size; + if (getRequestHeaderSize(&size) != WasmResult::Ok) { + logWarn("unexpected failure of getRequestHeaderMapSize"); + } + if (getResponseHeaderSize(&size) != WasmResult::BadArgument) { + logWarn("unexpected success of getResponseHeaderMapSize"); + } + if (server->view() == "envoy-wasm-pause") { + return FilterHeadersStatus::StopIteration; + } else if (server->view() == "envoy-wasm-end-stream") { + return FilterHeadersStatus::ContinueAndEndStream; + } else if (server->view() == "envoy-wasm-stop-buffer") { + return FilterHeadersStatus::StopAllIterationAndBuffer; + } else if (server->view() == "envoy-wasm-stop-watermark") { + return FilterHeadersStatus::StopAllIterationAndWatermark; + } else { + return FilterHeadersStatus::Continue; + } + } else if (test == "metadata") { + std::string value; + if (!getValue({"node", "metadata", "wasm_node_get_key"}, &value)) { + logDebug("missing node metadata"); + } + auto r = setFilterStateStringValue("wasm_request_set_key", "wasm_request_set_value"); + if (r != WasmResult::Ok) { + logDebug(toString(r)); + } + auto path = getRequestHeader(":path"); + logInfo(std::string("header path ") + path->toString()); + addRequestHeader("newheader", "newheadervalue"); + replaceRequestHeader("server", "envoy-wasm"); + + { + const std::string expr = R"("server is " + request.headers["server"])"; + uint32_t token = 0; + if (WasmResult::Ok != createExpression(expr, &token)) { + logError("expr_create error"); + } else { + std::string eval_result; + if (!evaluateExpression(token, &eval_result)) { + logError("expr_eval error"); + } else { + logInfo(eval_result); + } + if (WasmResult::Ok != exprDelete(token)) { + logError("failed to delete an expression"); + } + } + } + + { + // Validate a valid CEL expression + const std::string expr = R"( + envoy.api.v2.core.GrpcService{ + envoy_grpc: envoy.api.v2.core.GrpcService.EnvoyGrpc { + cluster_name: "test" + } + })"; + uint32_t token = 0; + if (WasmResult::Ok != createExpression(expr, &token)) { + logError("expr_create error"); + } else { + GrpcService eval_result; + if (!evaluateMessage(token, &eval_result)) { + logError("expr_eval error"); + } else { + logInfo("grpc service: " + eval_result.envoy_grpc().cluster_name()); + } + if (WasmResult::Ok != exprDelete(token)) { + logError("failed to delete an expression"); + } + } + } + + { + // Create a syntactically wrong CEL expression + uint32_t token = 0; + if (createExpression("/ /", &token) != WasmResult::BadArgument) { + logError("expect an error on a syntactically wrong expressions"); + } + } + + { + // Create an invalid CEL expression + uint32_t token = 0; + if (createExpression("_&&_(a, b, c)", &token) != WasmResult::BadArgument) { + logError("expect an error on invalid expressions"); + } + } + + { + // Evaluate a bad token + std::string result; + uint64_t token = 0; + if (evaluateExpression(token, &result)) { + logError("expect an error on invalid token in evaluate"); + } + } + + { + // Evaluate a missing token + std::string result; + uint32_t token = 0xFFFFFFFF; + if (evaluateExpression(token, &result)) { + logError("expect an error on unknown token in evaluate"); + } + // Delete a missing token + if (exprDelete(token) != WasmResult::Ok) { + logError("expect no error on unknown token in delete expression"); + } + } + + { + // Evaluate two expressions to an error + uint32_t token1 = 0; + if (createExpression("1/0", &token1) != WasmResult::Ok) { + logError("unexpected error on division by zero expression"); + } + uint32_t token2 = 0; + if (createExpression("request.duration.size", &token2) != WasmResult::Ok) { + logError("unexpected error on integer field access expression"); + } + std::string result; + if (evaluateExpression(token1, &result)) { + logError("expect an error on division by zero"); + } + if (evaluateExpression(token2, &result)) { + logError("expect an error on integer field access expression"); + } + if (exprDelete(token1) != WasmResult::Ok) { + logError("failed to delete an expression"); + } + if (exprDelete(token2) != WasmResult::Ok) { + logError("failed to delete an expression"); + } + } + + { + int64_t dur; + if (getValue({"request", "duration"}, &dur)) { + logInfo("duration is " + std::to_string(dur)); + } else { + logError("failed to get request duration"); + } + } + + return FilterHeadersStatus::Continue; + } + return FilterHeadersStatus::Continue; +} + +FilterTrailersStatus TestContext::onRequestTrailers(uint32_t) { + auto request_trailer = getRequestTrailer("bogus-trailer"); + if (request_trailer && request_trailer->view() != "") { + logWarn("request bogus-trailer found"); + } + CHECK_RESULT(replaceRequestTrailer("new-trailer", "value")); + CHECK_RESULT(removeRequestTrailer("x")); + // Not available yet. + replaceResponseTrailer("new-trailer", "value"); + auto response_trailer = getResponseTrailer("bogus-trailer"); + if (response_trailer && response_trailer->view() != "") { + logWarn("request bogus-trailer found"); + } + return FilterTrailersStatus::Continue; +} + +FilterTrailersStatus TestContext::onResponseTrailers(uint32_t) { + auto value = getResponseTrailer("bogus-trailer"); + if (value && value->view() != "") { + logWarn("response bogus-trailer found"); + } + CHECK_RESULT(replaceResponseTrailer("new-trailer", "value")); + return FilterTrailersStatus::StopIteration; +} + +FilterDataStatus TestContext::onRequestBody(size_t body_buffer_length, bool) { + auto test = root()->test_; + if (test == "headers") { + auto body = getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_buffer_length); + logError(std::string("onBody ") + std::string(body->view())); + } else if (test == "metadata") { + std::string value; + if (!getValue({"node", "metadata", "wasm_node_get_key"}, &value)) { + logDebug("missing node metadata"); + } + logError(std::string("onBody ") + value); + std::string request_string; + std::string request_string2; + if (!getValue( + {"metadata", "filter_metadata", "envoy.filters.http.wasm", "wasm_request_get_key"}, + &request_string)) { + logDebug("missing request metadata"); + } + if (!getValue( + {"metadata", "filter_metadata", "envoy.filters.http.wasm", "wasm_request_get_key"}, + &request_string2)) { + logDebug("missing request metadata"); + } + logTrace(std::string("Struct ") + request_string + " " + request_string2); + return FilterDataStatus::Continue; + } + return FilterDataStatus::Continue; +} + +void TestContext::onLog() { + auto test = root()->test_; + if (test == "headers") { + auto path = getRequestHeader(":path"); + logWarn("onLog " + std::to_string(id()) + " " + std::string(path->view())); + auto response_header = getResponseHeader("bogus-header"); + if (response_header && response_header->view() != "") { + logWarn("response bogus-header found"); + } + auto response_trailer = getResponseTrailer("bogus-trailer"); + if (response_trailer && response_trailer->view() != "") { + logWarn("response bogus-trailer found"); + } + } else if (test == "property") { + setFilterState("wasm_state", "wasm_value"); + auto path = getRequestHeader(":path"); + if (path->view() == "/test_context") { + logWarn("request.path: " + getProperty({"request", "path"}).value()->toString()); + logWarn("node.metadata: " + + getProperty({"node", "metadata", "istio.io/metadata"}).value()->toString()); + logWarn("metadata: " + getProperty({"metadata", "filter_metadata", "envoy.filters.http.wasm", + "wasm_request_get_key"}) + .value() + ->toString()); + int64_t responseCode; + if (getValue({"response", "code"}, &responseCode)) { + logWarn("response.code: " + std::to_string(responseCode)); + } + logWarn("state: " + getProperty({"wasm_state"}).value()->toString()); + } else { + logWarn("onLog " + std::to_string(id()) + " " + std::string(path->view())); + } + + // Wasm state property set and read validation for {i: 1337} + // Generated using the following input.json: + // { + // "i": 1337 + // } + // flatc -b schema.fbs input.json + { + static const char data[24] = {0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x0c, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x39, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + if (WasmResult::Ok != setFilterState("structured_state", std::string_view(data, 24))) { + logWarn("setProperty(structured_state) failed"); + } + int64_t value = 0; + if (!getValue({"structured_state", "i"}, &value)) { + logWarn("getProperty(structured_state) failed"); + } + if (value != 1337) { + logWarn("getProperty(structured_state) returned " + std::to_string(value)); + } + std::string buffer; + if (!getValue({"structured_state"}, &buffer)) { + logWarn("getValue for structured_state should not fail"); + } + if (buffer.size() != 24) { + logWarn("getValue for structured_state should return the buffer"); + } + } + { + if (setFilterState("string_state", "unicorns") != WasmResult::Ok) { + logWarn("setProperty(string_state) failed"); + } + std::string value; + if (!getValue({"string_state"}, &value)) { + logWarn("getProperty(string_state) failed"); + } + if (value != "unicorns") { + logWarn("getProperty(string_state) returned " + value); + } + } + { + // access via "filter_state" property + std::string value; + if (!getValue({"filter_state", "wasm.string_state"}, &value)) { + logWarn("accessing via filter_state failed"); + } + if (value != "unicorns") { + logWarn("unexpected value: " + value); + } + } + { + // attempt to write twice for a read only wasm state + if (setFilterState("string_state", "ponies") == WasmResult::Ok) { + logWarn("expected second setProperty(string_state) to fail"); + } + std::string value; + if (!getValue({"string_state"}, &value)) { + logWarn("getProperty(string_state) failed"); + } + if (value != "unicorns") { + logWarn("getProperty(string_state) returned " + value); + } + } + { + if (setFilterState("bytes_state", "ponies") != WasmResult::Ok) { + logWarn("setProperty(bytes_state) failed"); + } + std::string value; + if (!getValue({"bytes_state"}, &value)) { + logWarn("getProperty(bytes_state) failed"); + } + if (value != "ponies") { + logWarn("getProperty(bytes_state) returned " + value); + } + } + { + wasmtest::TestProto test_proto; + uint32_t i = 53; + test_proto.set_i(i); + double j = 13.0; + test_proto.set_j(j); + bool k = true; + test_proto.set_k(k); + std::string s = "centaur"; + test_proto.set_s(s); + test_proto.mutable_t()->set_seconds(2); + test_proto.mutable_t()->set_nanos(3); + test_proto.add_l("abc"); + test_proto.add_l("xyz"); + (*test_proto.mutable_m())["a"] = "b"; + + // validate setting a filter state + std::string in; + test_proto.SerializeToString(&in); + if (setFilterState("protobuf_state", in) != WasmResult::Ok) { + logWarn("setProperty(protobuf_state) failed"); + } + // validate uint field + uint64_t i2; + if (!getValue({"protobuf_state", "i"}, &i2) || i2 != i) { + logWarn("uint field returned " + std::to_string(i2)); + } + + // validate double field + double j2; + if (!getValue({"protobuf_state", "j"}, &j2) || j2 != j) { + logWarn("double field returned " + std::to_string(j2)); + } + + // validate bool field + bool k2; + if (!getValue({"protobuf_state", "k"}, &k2) || k2 != k) { + logWarn("bool field returned " + std::to_string(k2)); + } + + // validate string field + std::string s2; + if (!getValue({"protobuf_state", "s"}, &s2) || s2 != s) { + logWarn("string field returned " + s2); + } + + // validate timestamp field + int64_t t; + if (!getValue({"protobuf_state", "t"}, &t) || t != 2000000003ull) { + logWarn("timestamp field returned " + std::to_string(t)); + } + + // validate malformed field + std::string a; + if (getValue({"protobuf_state", "a"}, &a)) { + logWarn("expect serialization error for malformed type_url string, got " + a); + } + + // validate null field + std::string b; + if (!getValue({"protobuf_state", "b"}, &b) || b != "") { + logWarn("null field returned " + b); + } + + // validate list field + auto l = getProperty({"protobuf_state", "l"}); + if (l.has_value()) { + auto pairs = l.value()->pairs(); + if (pairs.size() != 2 || pairs[0].first != "abc" || pairs[1].first != "xyz") { + logWarn("list field did not return the expected value"); + } + } else { + logWarn("list field returned none"); + } + + // validate map field + auto m = getProperty({"protobuf_state", "m"}); + if (m.has_value()) { + auto pairs = m.value()->pairs(); + if (pairs.size() != 1 || pairs[0].first != "a" || pairs[0].second != "b") { + logWarn("map field did not return the expected value: " + std::to_string(pairs.size())); + } + } else { + logWarn("map field returned none"); + } + + // validate entire message + std::string buffer; + if (!getValue({"protobuf_state"}, &buffer)) { + logWarn("getValue for protobuf_state should not fail"); + } + if (buffer.size() != in.size()) { + logWarn("getValue for protobuf_state should return the buffer"); + } + } + { + // Some properties are not available in the stream context. + const std::vector properties = {"xxx", "request", "route_name", "node"}; + for (const auto& property : properties) { + if (getProperty({property, "xxx"}).has_value()) { + logWarn("getProperty should not return a value in the root context"); + } + } + } + { + // Some properties are defined in the stream context. + std::vector, std::string>> properties = { + {{"plugin_name"}, "plugin_name"}, + {{"plugin_vm_id"}, "vm_id"}, + {{"listener_direction"}, std::string("\x1\0\0\0\0\0\0\0\0", 8)}, // INBOUND + {{"listener_metadata"}, ""}, + {{"route_name"}, "route12"}, + {{"cluster_name"}, "fake_cluster"}, + {{"connection_id"}, std::string("\x4\0\0\0\0\0\0\0\0", 8)}, + {{"connection", "requested_server_name"}, "w3.org"}, + {{"source", "address"}, "127.0.0.1:0"}, + {{"destination", "address"}, "127.0.0.2:0"}, + {{"upstream", "address"}, "10.0.0.1:443"}, + {{"cluster_metadata"}, ""}, + {{"route_metadata"}, ""}, + }; + for (const auto& property : properties) { + std::string value; + if (!getValue(property.first, &value)) { + logWarn("getValue should provide a value in the root context: " + property.second); + } + if (value != property.second) { + logWarn("getValue returned " + value + ", expect " + property.second); + } + } + } + } +} + +void TestContext::onDone() { + auto test = root()->test_; + if (test == "headers") { + logWarn("onDone " + std::to_string(id())); + } +} + +void TestRootContext::onTick() { + if (test_ == "headers") { + getContext(stream_context_id_)->setEffectiveContext(); + replaceRequestHeader("server", "envoy-wasm-continue"); + continueRequest(); + if (getBufferBytes(WasmBufferType::PluginConfiguration, 0, 1)->view() != "") { + logDebug("unexpectd success of getBufferBytes PluginConfiguration"); + } + } else if (test_ == "metadata") { + std::string value; + if (!getValue({"node", "metadata", "wasm_node_get_key"}, &value)) { + logDebug("missing node metadata"); + } + logDebug(std::string("onTick ") + value); + } else if (test_ == "property") { + uint64_t t; + if (WasmResult::Ok != proxy_get_current_time_nanoseconds(&t)) { + logError(std::string("bad proxy_get_current_time_nanoseconds result")); + } + std::string function = "declare_property"; + { + envoy::source::extensions::common::wasm::DeclarePropertyArguments args; + args.set_name("structured_state"); + args.set_type(envoy::source::extensions::common::wasm::WasmType::FlatBuffers); + args.set_span(envoy::source::extensions::common::wasm::LifeSpan::DownstreamConnection); + // Reflection flatbuffer for a simple table {i : int64}. + // Generated using the following schema.fbs: + // + // namespace Wasm.Common; + // table T { + // i: int64; + // } + // root_type T; + // + // flatc --cpp --bfbs-gen-embed schema.fbs + static const char bfbsData[192] = { + 0x18, 0x00, 0x00, 0x00, 0x42, 0x46, 0x42, 0x53, 0x10, 0x00, 0x1C, 0x00, 0x04, 0x00, 0x08, + 0x00, 0x0C, 0x00, 0x10, 0x00, 0x14, 0x00, 0x18, 0x00, 0x10, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x34, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x10, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x0D, 0x00, 0x00, 0x00, 0x57, 0x61, 0x73, 0x6D, 0x2E, 0x43, 0x6F, 0x6D, 0x6D, 0x6F, 0x6E, + 0x2E, 0x54, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x12, 0x00, 0x08, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00}; + args.set_schema(bfbsData, 192); + std::string in; + args.SerializeToString(&in); + char* out = nullptr; + size_t out_size = 0; + if (WasmResult::Ok != proxy_call_foreign_function(function.data(), function.size(), in.data(), + in.size(), &out, &out_size)) { + logError("declare_property failed for flatbuffers"); + } + ::free(out); + } + { + envoy::source::extensions::common::wasm::DeclarePropertyArguments args; + args.set_name("string_state"); + args.set_type(envoy::source::extensions::common::wasm::WasmType::String); + args.set_span(envoy::source::extensions::common::wasm::LifeSpan::FilterChain); + args.set_readonly(true); + std::string in; + args.SerializeToString(&in); + char* out = nullptr; + size_t out_size = 0; + if (WasmResult::Ok != proxy_call_foreign_function(function.data(), function.size(), in.data(), + in.size(), &out, &out_size)) { + logError("declare_property failed for strings"); + } + ::free(out); + } + { + envoy::source::extensions::common::wasm::DeclarePropertyArguments args; + args.set_name("bytes_state"); + args.set_type(envoy::source::extensions::common::wasm::WasmType::Bytes); + args.set_span(envoy::source::extensions::common::wasm::LifeSpan::DownstreamRequest); + std::string in; + args.SerializeToString(&in); + char* out = nullptr; + size_t out_size = 0; + if (WasmResult::Ok != proxy_call_foreign_function(function.data(), function.size(), in.data(), + in.size(), &out, &out_size)) { + logError("declare_property failed for bytes"); + } + ::free(out); + } + { + // double declaration of "bytes_state" should return BAD_ARGUMENT + envoy::source::extensions::common::wasm::DeclarePropertyArguments args; + args.set_name("bytes_state"); + std::string in; + args.SerializeToString(&in); + char* out = nullptr; + size_t out_size = 0; + if (WasmResult::BadArgument != proxy_call_foreign_function(function.data(), function.size(), + in.data(), in.size(), &out, + &out_size)) { + logError("declare_property must fail for double declaration"); + } + ::free(out); + } + { + envoy::source::extensions::common::wasm::DeclarePropertyArguments args; + args.set_name("protobuf_state"); + args.set_type(envoy::source::extensions::common::wasm::WasmType::Protobuf); + args.set_span(envoy::source::extensions::common::wasm::LifeSpan::DownstreamRequest); + args.set_schema("type.googleapis.com/wasmtest.TestProto"); + std::string in; + args.SerializeToString(&in); + char* out = nullptr; + size_t out_size = 0; + if (WasmResult::Ok != proxy_call_foreign_function(function.data(), function.size(), in.data(), + in.size(), &out, &out_size)) { + logError("declare_property failed for protobuf"); + } + ::free(out); + } + { + char* out = nullptr; + size_t out_size = 0; + if (WasmResult::Ok == proxy_call_foreign_function(function.data(), function.size(), + function.data(), function.size(), &out, + &out_size)) { + logError("expected declare_property to fail"); + } + ::free(out); + } + { + // setting a filter state in root context returns NOT_FOUND + if (setFilterState("string_state", "unicorns") != WasmResult::NotFound) { + logWarn("setProperty(string_state) should fail in root context"); + } + } + } +} + +class Context1 : public Context { +public: + Context1(uint32_t id, RootContext* root) : Context(id, root) {} + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; +}; + +class Context2 : public Context { +public: + Context2(uint32_t id, RootContext* root) : Context(id, root) {} + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; +}; + +static RegisterContextFactory register_Context1(CONTEXT_FACTORY(Context1), "context1"); +static RegisterContextFactory register_Contxt2(CONTEXT_FACTORY(Context2), "context2"); + +FilterHeadersStatus Context1::onRequestHeaders(uint32_t, bool) { + logDebug(std::string("onRequestHeaders1 ") + std::to_string(id())); + return FilterHeadersStatus::Continue; +} + +FilterHeadersStatus Context2::onRequestHeaders(uint32_t, bool) { + logDebug(std::string("onRequestHeaders2 ") + std::to_string(id())); + CHECK_RESULT(sendLocalResponse(200, "ok", "body", {{"foo", "bar"}})); + return FilterHeadersStatus::Continue; +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/test_data/test_cpp_null_plugin.cc b/test/extensions/filters/http/wasm/test_data/test_cpp_null_plugin.cc new file mode 100644 index 000000000000..38f1e82c78b3 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace HttpWasmTestCpp { +NullPluginRegistry* context_registry_; +} // namespace HttpWasmTestCpp + +RegisterNullVmPluginFactory register_common_wasm_test_cpp_plugin("HttpWasmTestCpp", []() { + return std::make_unique(HttpWasmTestCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/filters/http/wasm/test_data/test_grpc_call_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_grpc_call_cpp.cc new file mode 100644 index 000000000000..0abdc79ce7c2 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_grpc_call_cpp.cc @@ -0,0 +1,83 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +class MyGrpcCallHandler : public GrpcCallHandler { +public: + MyGrpcCallHandler() : GrpcCallHandler() {} + void onSuccess(size_t body_size) override { + auto response = getBufferBytes(WasmBufferType::GrpcReceiveBuffer, 0, body_size); + logDebug(response->proto().string_value()); + cancel(); + } + void onFailure(GrpcStatus) override { + auto p = getStatus(); + logDebug(std::string("failure ") + std::string(p.second->view())); + } +}; + +class GrpcCallRootContext : public RootContext { +public: + explicit GrpcCallRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {} + + void onQueueReady(uint32_t op) override { + if (op == 0) { + handler_->cancel(); + } else { + grpcClose(handler_->token()); + } + } + + MyGrpcCallHandler* handler_ = nullptr; +}; + +class GrpcCallContext : public Context { +public: + explicit GrpcCallContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; + + GrpcCallRootContext* root() { return static_cast(Context::root()); } +}; + +static RegisterContextFactory register_GrpcCallContext(CONTEXT_FACTORY(GrpcCallContext), + ROOT_FACTORY(GrpcCallRootContext), + "grpc_call"); + +FilterHeadersStatus GrpcCallContext::onRequestHeaders(uint32_t, bool end_of_stream) { + GrpcService grpc_service; + grpc_service.mutable_envoy_grpc()->set_cluster_name("cluster"); + std::string grpc_service_string; + grpc_service.SerializeToString(&grpc_service_string); + google::protobuf::Value value; + value.set_string_value("request"); + HeaderStringPairs initial_metadata; + root()->handler_ = new MyGrpcCallHandler(); + if (end_of_stream) { + if (root()->grpcCallHandler(grpc_service_string, "service", "method", initial_metadata, value, + 1000, std::unique_ptr(root()->handler_)) == + WasmResult::Ok) { + logError("expected failure did not occur"); + } + return FilterHeadersStatus::Continue; + } + root()->grpcCallHandler(grpc_service_string, "service", "method", initial_metadata, value, 1000, + std::unique_ptr(root()->handler_)); + if (root()->grpcCallHandler( + "bogus grpc_service", "service", "method", initial_metadata, value, 1000, + std::unique_ptr(new MyGrpcCallHandler())) == WasmResult::Ok) { + logError("bogus grpc_service accepted error"); + } + return FilterHeadersStatus::StopIteration; +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/test_data/test_grpc_stream_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_grpc_stream_cpp.cc new file mode 100644 index 000000000000..6a357de65b87 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_grpc_stream_cpp.cc @@ -0,0 +1,94 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +class GrpcStreamContext : public Context { +public: + explicit GrpcStreamContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; +}; + +class GrpcStreamRootContext : public RootContext { +public: + explicit GrpcStreamRootContext(uint32_t id, std::string_view root_id) + : RootContext(id, root_id) {} +}; + +static RegisterContextFactory register_GrpcStreamContext(CONTEXT_FACTORY(GrpcStreamContext), + ROOT_FACTORY(GrpcStreamRootContext), + "grpc_stream"); +class MyGrpcStreamHandler + : public GrpcStreamHandler { +public: + MyGrpcStreamHandler() : GrpcStreamHandler() {} + void onReceiveInitialMetadata(uint32_t) override { + auto h = getHeaderMapValue(WasmHeaderMapType::GrpcReceiveInitialMetadata, "test"); + if (h->view() == "reset") { + reset(); + return; + } + // Not Found. + h = getHeaderMapValue(WasmHeaderMapType::HttpCallResponseHeaders, "foo"); + h = getHeaderMapValue(WasmHeaderMapType::HttpCallResponseTrailers, "foo"); + addHeaderMapValue(WasmHeaderMapType::GrpcReceiveInitialMetadata, "foo", "bar"); + } + void onReceive(size_t body_size) override { + auto response = getBufferBytes(WasmBufferType::GrpcReceiveBuffer, 0, body_size); + auto response_string = response->proto().string_value(); + google::protobuf::Value message; + if (response_string == "close") { + close(); + } else { + send(message, false); + } + logDebug(std::string("response ") + response_string); + } + void onReceiveTrailingMetadata(uint32_t) override { + auto h = getHeaderMapValue(WasmHeaderMapType::GrpcReceiveTrailingMetadata, "foo"); + addHeaderMapValue(WasmHeaderMapType::GrpcReceiveTrailingMetadata, "foo", "bar"); + } + void onRemoteClose(GrpcStatus) override { + auto p = getStatus(); + logDebug(std::string("close ") + std::string(p.second->view())); + if (p.second->view() == "close") { + close(); + } else if (p.second->view() == "ok") { + return; + } else { + reset(); + } + } +}; + +FilterHeadersStatus GrpcStreamContext::onRequestHeaders(uint32_t, bool) { + GrpcService grpc_service; + grpc_service.mutable_envoy_grpc()->set_cluster_name("cluster"); + std::string grpc_service_string; + grpc_service.SerializeToString(&grpc_service_string); + HeaderStringPairs initial_metadata; + if (root()->grpcStreamHandler("bogus service string", "service", "method", initial_metadata, + std::unique_ptr( + new MyGrpcStreamHandler())) != WasmResult::ParseFailure) { + logError("unexpected bogus service string OK"); + } + if (root()->grpcStreamHandler(grpc_service_string, "service", "bad method", initial_metadata, + std::unique_ptr( + new MyGrpcStreamHandler())) != WasmResult::InternalFailure) { + logError("unexpected bogus method OK"); + } + root()->grpcStreamHandler(grpc_service_string, "service", "method", initial_metadata, + std::unique_ptr(new MyGrpcStreamHandler())); + return FilterHeadersStatus::StopIteration; +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/test_data/test_shared_data_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_shared_data_cpp.cc new file mode 100644 index 000000000000..6ecf802903d9 --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_shared_data_cpp.cc @@ -0,0 +1,55 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +class SharedDataRootContext : public RootContext { +public: + explicit SharedDataRootContext(uint32_t id, std::string_view root_id) + : RootContext(id, root_id) {} + + void onTick() override; + void onQueueReady(uint32_t) override; +}; + +static RegisterContextFactory register_SharedDataRootContext(ROOT_FACTORY(SharedDataRootContext), + "shared_data"); + +void SharedDataRootContext::onTick() { + setHeaderMapPairs(WasmHeaderMapType::GrpcReceiveInitialMetadata, {}); + setRequestHeaderPairs({{"foo", "bar"}}); + WasmDataPtr value0; + if (getSharedData("shared_data_key_bad", &value0) == WasmResult::NotFound) { + logDebug("get of bad key not found"); + } + CHECK_RESULT(setSharedData("shared_data_key1", "shared_data_value0")); + CHECK_RESULT(setSharedData("shared_data_key1", "shared_data_value1")); + CHECK_RESULT(setSharedData("shared_data_key2", "shared_data_value2")); + uint32_t cas = 0; + auto value2 = getSharedDataValue("shared_data_key2", &cas); + if (WasmResult::CasMismatch == + setSharedData("shared_data_key2", "shared_data_value3", cas + 1)) { // Bad cas. + logInfo("set CasMismatch"); + } +} + +void SharedDataRootContext::onQueueReady(uint32_t) { + WasmDataPtr value0; + if (getSharedData("shared_data_key_bad", &value0) == WasmResult::NotFound) { + logDebug("second get of bad key not found"); + } + auto value1 = getSharedDataValue("shared_data_key1"); + logDebug("get 1 " + value1->toString()); + auto value2 = getSharedDataValue("shared_data_key2"); + logCritical("get 2 " + value2->toString()); +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/test_data/test_shared_queue_cpp.cc b/test/extensions/filters/http/wasm/test_data/test_shared_queue_cpp.cc new file mode 100644 index 000000000000..ea171e251bff --- /dev/null +++ b/test/extensions/filters/http/wasm/test_data/test_shared_queue_cpp.cc @@ -0,0 +1,69 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics_lite.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(HttpWasmTestCpp) + +class SharedQueueContext : public Context { +public: + explicit SharedQueueContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; +}; + +class SharedQueueRootContext : public RootContext { +public: + explicit SharedQueueRootContext(uint32_t id, std::string_view root_id) + : RootContext(id, root_id) {} + + bool onStart(size_t) override; + void onQueueReady(uint32_t) override; + + uint32_t shared_queue_token_; +}; + +static RegisterContextFactory register_SharedQueueContext(CONTEXT_FACTORY(SharedQueueContext), + ROOT_FACTORY(SharedQueueRootContext), + "shared_queue"); + +bool SharedQueueRootContext::onStart(size_t) { + CHECK_RESULT(registerSharedQueue("my_shared_queue", &shared_queue_token_)); + return true; +} + +FilterHeadersStatus SharedQueueContext::onRequestHeaders(uint32_t, bool) { + uint32_t token; + if (resolveSharedQueue("vm_id", "bad_shared_queue", &token) == WasmResult::NotFound) { + logWarn("onRequestHeaders not found bad_shared_queue"); + } + CHECK_RESULT(resolveSharedQueue("vm_id", "my_shared_queue", &token)); + if (enqueueSharedQueue(token, "data1") == WasmResult::Ok) { + logWarn("onRequestHeaders enqueue Ok"); + } + return FilterHeadersStatus::Continue; +} + +void SharedQueueRootContext::onQueueReady(uint32_t token) { + if (token == shared_queue_token_) { + logInfo("onQueueReady"); + } + std::unique_ptr data; + if (dequeueSharedQueue(9999999 /* bad token */, &data) == WasmResult::NotFound) { + logWarn("onQueueReady bad token not found"); + } + if (dequeueSharedQueue(token, &data) == WasmResult::Ok) { + logDebug("data " + data->toString() + " Ok"); + } + if (dequeueSharedQueue(token, &data) == WasmResult::Empty) { + logWarn("onQueueReady extra data not found"); + } +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/http/wasm/wasm_filter_test.cc b/test/extensions/filters/http/wasm/wasm_filter_test.cc new file mode 100644 index 000000000000..37a2e465e715 --- /dev/null +++ b/test/extensions/filters/http/wasm/wasm_filter_test.cc @@ -0,0 +1,1393 @@ +#include "common/http/message_impl.h" + +#include "extensions/filters/http/wasm/wasm_filter.h" + +#include "test/mocks/network/connection.h" +#include "test/mocks/router/mocks.h" +#include "test/test_common/wasm_base.h" + +using testing::Eq; +using testing::Invoke; +using testing::Return; +using testing::ReturnRef; + +MATCHER_P(MapEq, rhs, "") { + const Envoy::ProtobufWkt::Struct& obj = arg; + EXPECT_TRUE(rhs.size() > 0); + for (auto const& entry : rhs) { + EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); + } + return true; +} + +using BufferFunction = std::function; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Wasm { + +using Envoy::Extensions::Common::Wasm::CreateContextFn; +using Envoy::Extensions::Common::Wasm::Plugin; +using Envoy::Extensions::Common::Wasm::PluginSharedPtr; +using Envoy::Extensions::Common::Wasm::Wasm; +using Envoy::Extensions::Common::Wasm::WasmHandleSharedPtr; +using proxy_wasm::ContextBase; +using GrpcService = envoy::config::core::v3::GrpcService; +using WasmFilterConfig = envoy::extensions::filters::http::wasm::v3::Wasm; + +class TestFilter : public Envoy::Extensions::Common::Wasm::Context { +public: + TestFilter(Wasm* wasm, uint32_t root_context_id, + Envoy::Extensions::Common::Wasm::PluginSharedPtr plugin) + : Envoy::Extensions::Common::Wasm::Context(wasm, root_context_id, plugin) {} + MOCK_CONTEXT_LOG_; +}; + +class TestRoot : public Envoy::Extensions::Common::Wasm::Context { +public: + TestRoot(Wasm* wasm, const std::shared_ptr& plugin) : Context(wasm, plugin) {} + MOCK_CONTEXT_LOG_; +}; + +class WasmHttpFilterTest : public Common::Wasm::WasmHttpFilterTestBase< + testing::TestWithParam>> { +public: + WasmHttpFilterTest() = default; + ~WasmHttpFilterTest() override = default; + + CreateContextFn createContextFn() { + return [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + return new TestRoot(wasm, plugin); + }; + } + + void setup(const std::string& code, std::string root_id = "", std::string vm_configuration = "") { + setupBase(std::get<0>(GetParam()), code, createContextFn(), root_id, vm_configuration); + } + void setupTest(std::string root_id = "", std::string vm_configuration = "") { + std::string code; + if (std::get<0>(GetParam()) == "null") { + code = "HttpWasmTestCpp"; + } else { + if (std::get<1>(GetParam()) == "cpp") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath( + "test/extensions/filters/http/wasm/test_data/test_cpp.wasm")); + } else { + auto filename = !root_id.empty() ? root_id : vm_configuration; + const auto basic_path = TestEnvironment::runfilesPath( + absl::StrCat("test/extensions/filters/http/wasm/test_data/", filename)); + code = TestEnvironment::readFileToStringForTest(basic_path + "_rust.wasm"); + } + } + setupBase(std::get<0>(GetParam()), code, createContextFn(), root_id, vm_configuration); + } + void setupFilter(const std::string root_id = "") { setupFilterBase(root_id); } + + void setupGrpcStreamTest(Grpc::RawAsyncStreamCallbacks*& callbacks); + + TestRoot& rootContext() { return *static_cast(root_context_); } + TestFilter& filter() { return *static_cast(context_.get()); } + +protected: + NiceMock async_stream_; + Grpc::MockAsyncClientManager async_client_manager_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + std::make_tuple("v8", "cpp"), std::make_tuple("v8", "rust"), +#endif +#if defined(ENVOY_WASM_WAVM) + std::make_tuple("wavm", "cpp"), std::make_tuple("wavm", "rust"), +#endif + std::make_tuple("null", "cpp")); +INSTANTIATE_TEST_SUITE_P(RuntimesAndLanguages, WasmHttpFilterTest, testing_values); + +// Bad code in initial config. +TEST_P(WasmHttpFilterTest, BadCode) { + setup("bad code"); + EXPECT_EQ(wasm_, nullptr); +} + +// Script touching headers only, request that is headers only. +TEST_P(WasmHttpFilterTest, HeadersOnlyRequestHeadersOnly) { + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, {"server", "envoy"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); + EXPECT_THAT(request_headers.get_("newheader"), Eq("newheadervalue")); + EXPECT_THAT(request_headers.get_("server"), Eq("envoy-wasm")); + // Test some errors. + EXPECT_EQ(filter().continueStream(static_cast(9999)), + proxy_wasm::WasmResult::BadArgument); + EXPECT_EQ(filter().closeStream(static_cast(9999)), + proxy_wasm::WasmResult::BadArgument); + Http::TestResponseHeaderMapImpl response_headers; + EXPECT_EQ(filter().encode100ContinueHeaders(response_headers), + Http::FilterHeadersStatus::Continue); + filter().onDestroy(); +} + +TEST_P(WasmHttpFilterTest, AllHeadersAndTrailers) { + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, {"server", "envoy"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + EXPECT_THAT(request_headers.get_("newheader"), Eq("newheadervalue")); + EXPECT_THAT(request_headers.get_("server"), Eq("envoy-wasm")); + Http::TestRequestTrailerMapImpl request_trailers{}; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter().decodeTrailers(request_trailers)); + Http::MetadataMap request_metadata{}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter().decodeMetadata(request_metadata)); + Http::TestResponseHeaderMapImpl response_headers{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().encodeHeaders(response_headers, false)); + Http::TestResponseTrailerMapImpl response_trailers{}; + EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, filter().encodeTrailers(response_trailers)); + Http::MetadataMap response_metadata{}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter().encodeMetadata(response_metadata)); + filter().onDestroy(); +} + +TEST_P(WasmHttpFilterTest, AllHeadersAndTrailersNotStarted) { + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + Http::TestRequestTrailerMapImpl request_trailers{}; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter().decodeTrailers(request_trailers)); + Http::MetadataMap request_metadata{}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter().decodeMetadata(request_metadata)); + Http::TestResponseHeaderMapImpl response_headers{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().encodeHeaders(response_headers, false)); + Http::TestResponseTrailerMapImpl response_trailers{}; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter().encodeTrailers(response_trailers)); + Http::MetadataMap response_metadata{}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter().encodeMetadata(response_metadata)); + Buffer::OwnedImpl data("data"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data, false)); + filter().onDestroy(); +} + +// Script touching headers only, request that is headers only. +TEST_P(WasmHttpFilterTest, HeadersOnlyRequestHeadersAndBody) { + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + EXPECT_FALSE(filter().endOfStream(proxy_wasm::WasmStreamType::Request)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + filter().onDestroy(); +} + +TEST_P(WasmHttpFilterTest, HeadersStopAndContinue) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): This hand off is not currently possible in the Rust SDK. + return; + } + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, {"server", "envoy-wasm-pause"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, true)); + root_context_->onTick(0); + filter().clearRouteCache(); + EXPECT_THAT(request_headers.get_("newheader"), Eq("newheadervalue")); + EXPECT_THAT(request_headers.get_("server"), Eq("envoy-wasm-continue")); + filter().onDestroy(); +} + +#if 0 +TEST_P(WasmHttpFilterTest, HeadersStopAndEndStream) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): This hand off is not currently possible in the Rust SDK. + return; + } + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"server", "envoy-wasm-end-stream"}}; + EXPECT_EQ(Http::FilterHeadersStatus::ContinueAndEndStream, + filter().decodeHeaders(request_headers, true)); + root_context_->onTick(0); + EXPECT_THAT(request_headers.get_("newheader"), Eq("newheadervalue")); + EXPECT_THAT(request_headers.get_("server"), Eq("envoy-wasm-continue")); + filter().onDestroy(); +} +#endif + +TEST_P(WasmHttpFilterTest, HeadersStopAndBuffer) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): This hand off is not currently possible in the Rust SDK. + return; + } + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"server", "envoy-wasm-stop-buffer"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, + filter().decodeHeaders(request_headers, true)); + root_context_->onTick(0); + EXPECT_THAT(request_headers.get_("newheader"), Eq("newheadervalue")); + EXPECT_THAT(request_headers.get_("server"), Eq("envoy-wasm-continue")); + filter().onDestroy(); +} + +TEST_P(WasmHttpFilterTest, HeadersStopAndWatermark) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): This hand off is not currently possible in the Rust SDK. + return; + } + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"server", "envoy-wasm-stop-watermark"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter().decodeHeaders(request_headers, true)); + root_context_->onTick(0); + EXPECT_THAT(request_headers.get_("newheader"), Eq("newheadervalue")); + EXPECT_THAT(request_headers.get_("server"), Eq("envoy-wasm-continue")); + filter().onDestroy(); +} + +// Script that reads the body. +TEST_P(WasmHttpFilterTest, BodyRequestReadBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, {"x-test-operation", "ReadBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + filter().onDestroy(); +} + +// Script that prepends and appends to the body. +TEST_P(WasmHttpFilterTest, BodyRequestPrependAndAppendToBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), + log_(spdlog::level::err, Eq(absl::string_view("onBody prepend.hello.append")))); + EXPECT_CALL(filter(), log_(spdlog::level::err, + Eq(absl::string_view("onBody prepend.prepend.hello.append.append")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"x-test-operation", "PrependAndAppendToBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + if (std::get<1>(GetParam()) == "rust") { + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data, true)); + } else { + // This status is not available in the rust SDK. + // TODO: update all SDKs to the new revision of the spec and update the tests accordingly. + EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, filter().decodeData(data, true)); + EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, filter().encodeData(data, true)); + } + filter().onDestroy(); +} + +// Script that replaces the body. +TEST_P(WasmHttpFilterTest, BodyRequestReplaceBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody replace")))).Times(2); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"x-test-operation", "ReplaceBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + if (std::get<1>(GetParam()) == "rust") { + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data, true)); + } else { + // This status is not available in the rust SDK. + // TODO: update all SDKs to the new revision of the spec and update the tests accordingly. + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter().decodeData(data, true)); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter().encodeData(data, true)); + } + filter().onDestroy(); +} + +// Script that removes the body. +TEST_P(WasmHttpFilterTest, BodyRequestRemoveBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody ")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"x-test-operation", "RemoveBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + filter().onDestroy(); +} + +// Script that buffers the body. +TEST_P(WasmHttpFilterTest, BodyRequestBufferBody) { + setupTest("body"); + setupFilter("body"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"x-test-operation", "BufferBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + + Buffer::OwnedImpl bufferedBody; + EXPECT_CALL(decoder_callbacks_, decodingBuffer()).WillRepeatedly(Return(&bufferedBody)); + EXPECT_CALL(decoder_callbacks_, modifyDecodingBuffer(_)) + .WillRepeatedly(Invoke([&bufferedBody](BufferFunction f) { f(bufferedBody); })); + + Buffer::OwnedImpl data1("hello"); + bufferedBody.add(data1); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello")))).Times(1); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter().decodeData(data1, false)); + + Buffer::OwnedImpl data2(" again "); + bufferedBody.add(data2); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello again ")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter().decodeData(data2, false)); + + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello again hello")))) + .Times(1); + Buffer::OwnedImpl data3("hello"); + bufferedBody.add(data3); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data3, true)); + + // Verify that the response still works even though we buffered the request. + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, + {"x-test-operation", "ReadBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().encodeHeaders(response_headers, false)); + // Should not buffer this time + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello")))).Times(2); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data1, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data1, true)); + + filter().onDestroy(); +} + +// Script that prepends and appends to the buffered body. +TEST_P(WasmHttpFilterTest, BodyRequestPrependAndAppendToBufferedBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), + log_(spdlog::level::err, Eq(absl::string_view("onBody prepend.hello.append")))); + Http::TestRequestHeaderMapImpl request_headers{ + {":path", "/"}, {"x-test-operation", "PrependAndAppendToBufferedBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + filter().onDestroy(); +} + +// Script that replaces the buffered body. +TEST_P(WasmHttpFilterTest, BodyRequestReplaceBufferedBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody replace")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"x-test-operation", "ReplaceBufferedBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + filter().onDestroy(); +} + +// Script that removes the buffered body. +TEST_P(WasmHttpFilterTest, BodyRequestRemoveBufferedBody) { + setupTest("body"); + setupFilter("body"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody ")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, + {"x-test-operation", "RemoveBufferedBody"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + filter().onDestroy(); +} + +// Script that buffers the first part of the body and streams the rest +TEST_P(WasmHttpFilterTest, BodyRequestBufferThenStreamBody) { + setupTest("body"); + setupFilter("body"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + + Buffer::OwnedImpl bufferedBody; + EXPECT_CALL(decoder_callbacks_, decodingBuffer()).WillRepeatedly(Return(&bufferedBody)); + EXPECT_CALL(decoder_callbacks_, modifyDecodingBuffer(_)) + .WillRepeatedly(Invoke([&bufferedBody](BufferFunction f) { f(bufferedBody); })); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, + {"x-test-operation", "BufferTwoBodies"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().encodeHeaders(response_headers, false)); + + Buffer::OwnedImpl data1("hello"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello")))).Times(1); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter().decodeData(data1, false)); + bufferedBody.add(data1); + + Buffer::OwnedImpl data2(", there, "); + bufferedBody.add(data2); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello, there, ")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter().decodeData(data2, false)); + + // Previous callbacks returned "Buffer" so we have buffered so far + Buffer::OwnedImpl data3("world!"); + bufferedBody.add(data3); + EXPECT_CALL(filter(), + log_(spdlog::level::err, Eq(absl::string_view("onBody hello, there, world!")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data3, false)); + + // Last callback returned "continue" so we just see individual chunks. + Buffer::OwnedImpl data4("So it's "); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody So it's ")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data4, false)); + + Buffer::OwnedImpl data5("goodbye, then!"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody goodbye, then!")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data5, true)); + + filter().onDestroy(); +} + +// Script that buffers the first part of the body and streams the rest +TEST_P(WasmHttpFilterTest, BodyResponseBufferThenStreamBody) { + setupTest("body"); + setupFilter("body"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + + Buffer::OwnedImpl bufferedBody; + EXPECT_CALL(encoder_callbacks_, modifyEncodingBuffer(_)) + .WillRepeatedly(Invoke([&bufferedBody](BufferFunction f) { f(bufferedBody); })); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, + {"x-test-operation", "BufferTwoBodies"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().encodeHeaders(response_headers, false)); + + Buffer::OwnedImpl data1("hello"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello")))).Times(1); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter().encodeData(data1, false)); + bufferedBody.add(data1); + + Buffer::OwnedImpl data2(", there, "); + bufferedBody.add(data2); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody hello, there, ")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter().encodeData(data2, false)); + + // Previous callbacks returned "Buffer" so we have buffered so far + Buffer::OwnedImpl data3("world!"); + bufferedBody.add(data3); + EXPECT_CALL(filter(), + log_(spdlog::level::err, Eq(absl::string_view("onBody hello, there, world!")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data3, false)); + + // Last callback returned "continue" so we just see individual chunks. + Buffer::OwnedImpl data4("So it's "); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody So it's ")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data4, false)); + + Buffer::OwnedImpl data5("goodbye, then!"); + EXPECT_CALL(filter(), log_(spdlog::level::err, Eq(absl::string_view("onBody goodbye, then!")))) + .Times(1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().encodeData(data5, true)); + + filter().onDestroy(); +} + +// Script testing AccessLog::Instance::log. +TEST_P(WasmHttpFilterTest, AccessLog) { + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(filter(), + log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders 2 headers")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onLog 2 /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + Http::TestResponseHeaderMapImpl response_headers{}; + Http::TestResponseTrailerMapImpl response_trailers{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + filter().continueStream(proxy_wasm::WasmStreamType::Response); + filter().closeStream(proxy_wasm::WasmStreamType::Response); + StreamInfo::MockStreamInfo log_stream_info; + filter().log(&request_headers, &response_headers, &response_trailers, log_stream_info); + filter().onDestroy(); +} + +TEST_P(WasmHttpFilterTest, AccessLogCreate) { + setupTest("", "headers"); + setupFilter(); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onLog 2 /")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("onDone 2")))); + + StreamInfo::MockStreamInfo log_stream_info; + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + Http::TestResponseHeaderMapImpl response_headers{}; + Http::TestResponseTrailerMapImpl response_trailers{}; + filter().log(&request_headers, &response_headers, &response_trailers, log_stream_info); + filter().onDestroy(); +} + +TEST_P(WasmHttpFilterTest, AsyncCall) { + setupTest("async_call"); + setupFilter("async_call"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + Http::MockAsyncClientRequest request(&cluster_manager_.async_client_); + Http::AsyncClient::Callbacks* callbacks = nullptr; + EXPECT_CALL(cluster_manager_, get(Eq("cluster"))).Times(testing::AtLeast(1)); + EXPECT_CALL(cluster_manager_, get(Eq("bogus cluster"))).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster")); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& cb, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + EXPECT_EQ((Http::TestRequestHeaderMapImpl{{":method", "POST"}, + {":path", "/"}, + {":authority", "foo"}, + {"content-length", "11"}}), + message->headers()); + EXPECT_EQ((Http::TestRequestTrailerMapImpl{{"trail", "cow"}}), *message->trailers()); + callbacks = &cb; + return &request; + })); + + EXPECT_CALL(filter(), log_(spdlog::level::debug, Eq("response"))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(":status -> 200"))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq("onRequestHeaders"))) + .WillOnce(Invoke([&](uint32_t, absl::string_view) -> proxy_wasm::WasmResult { + Http::ResponseMessagePtr response_message(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response_message->body().add("response"); + NiceMock span; + Http::TestResponseHeaderMapImpl response_header{{":status", "200"}}; + callbacks->onBeforeFinalizeUpstreamSpan(span, &response_header); + callbacks->onSuccess(request, std::move(response_message)); + return proxy_wasm::WasmResult::Ok; + })); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + EXPECT_NE(callbacks, nullptr); +} + +TEST_P(WasmHttpFilterTest, AsyncCallBadCall) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): The Rust SDK does not support end_of_stream in on_http_request_headers. + return; + } + setupTest("async_call"); + setupFilter("async_call"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + Http::MockAsyncClientRequest request(&cluster_manager_.async_client_); + EXPECT_CALL(cluster_manager_, get(Eq("cluster"))).Times(testing::AtLeast(1)); + EXPECT_CALL(cluster_manager_, get(Eq("bogus cluster"))).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster")); + // Just fail the send. + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks&, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + return nullptr; + })); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); +} + +TEST_P(WasmHttpFilterTest, AsyncCallFailure) { + setupTest("async_call"); + setupFilter("async_call"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + Http::MockAsyncClientRequest request(&cluster_manager_.async_client_); + Http::AsyncClient::Callbacks* callbacks = nullptr; + EXPECT_CALL(cluster_manager_, get(Eq("cluster"))).Times(testing::AtLeast(1)); + EXPECT_CALL(cluster_manager_, get(Eq("bogus cluster"))).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster")); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& cb, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + EXPECT_EQ((Http::TestRequestHeaderMapImpl{{":method", "POST"}, + {":path", "/"}, + {":authority", "foo"}, + {"content-length", "11"}}), + message->headers()); + EXPECT_EQ((Http::TestRequestTrailerMapImpl{{"trail", "cow"}}), *message->trailers()); + callbacks = &cb; + return &request; + })); + + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq("onRequestHeaders"))) + .WillOnce(Invoke([&](uint32_t, absl::string_view) -> proxy_wasm::WasmResult { + callbacks->onFailure(request, Http::AsyncClient::FailureReason::Reset); + return proxy_wasm::WasmResult::Ok; + })); + // TODO(PiotrSikora): RootContext handling is incomplete in the Rust SDK. + if (std::get<1>(GetParam()) == "rust") { + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq("async_call failed"))); + } else { + EXPECT_CALL(rootContext(), log_(spdlog::level::info, Eq("async_call failed"))); + } + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + EXPECT_NE(callbacks, nullptr); +} + +TEST_P(WasmHttpFilterTest, AsyncCallAfterDestroyed) { + setupTest("async_call"); + setupFilter("async_call"); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + Http::MockAsyncClientRequest request(&cluster_manager_.async_client_); + Http::AsyncClient::Callbacks* callbacks = nullptr; + EXPECT_CALL(cluster_manager_, get(Eq("cluster"))).Times(testing::AtLeast(1)); + EXPECT_CALL(cluster_manager_, get(Eq("bogus cluster"))).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(cluster_manager_, httpAsyncClientForCluster("cluster")); + EXPECT_CALL(cluster_manager_.async_client_, send_(_, _, _)) + .WillOnce( + Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& cb, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + EXPECT_EQ((Http::TestRequestHeaderMapImpl{{":method", "POST"}, + {":path", "/"}, + {":authority", "foo"}, + {"content-length", "11"}}), + message->headers()); + EXPECT_EQ((Http::TestRequestTrailerMapImpl{{"trail", "cow"}}), *message->trailers()); + callbacks = &cb; + return &request; + })); + + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq("onRequestHeaders"))); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + EXPECT_CALL(request, cancel()).WillOnce([&]() { callbacks = nullptr; }); + + // Destroy the Context, Plugin and VM. + context_.reset(); + plugin_.reset(); + wasm_.reset(); + + Http::ResponseMessagePtr response_message(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "200"}}})); + response_message->body().add("response"); + + // (Don't) Make the callback on the destroyed VM. + EXPECT_EQ(callbacks, nullptr); + if (callbacks) { + callbacks->onSuccess(request, std::move(response_message)); + } +} + +TEST_P(WasmHttpFilterTest, GrpcCall) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + setupTest("grpc_call"); + setupFilter("grpc_call"); + NiceMock request; + Grpc::RawAsyncRequestCallbacks* callbacks = nullptr; + Grpc::MockAsyncClientManager client_manager; + auto client_factory = std::make_unique(); + auto async_client = std::make_unique(); + Tracing::Span* parent_span{}; + EXPECT_CALL(*async_client, sendRaw(_, _, _, _, _, _)) + .WillOnce(Invoke([&](absl::string_view service_full_name, absl::string_view method_name, + Buffer::InstancePtr&& message, Grpc::RawAsyncRequestCallbacks& cb, + Tracing::Span& span, const Http::AsyncClient::RequestOptions& options) + -> Grpc::AsyncRequest* { + EXPECT_EQ(service_full_name, "service"); + EXPECT_EQ(method_name, "method"); + ProtobufWkt::Value value; + EXPECT_TRUE(value.ParseFromArray(message->linearize(message->length()), message->length())); + EXPECT_EQ(value.string_value(), "request"); + callbacks = &cb; + parent_span = &span; + EXPECT_EQ(options.timeout->count(), 1000); + return &request; + })); + EXPECT_CALL(*client_factory, create).WillOnce(Invoke([&]() -> Grpc::RawAsyncClientPtr { + return std::move(async_client); + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillOnce(Invoke([&]() -> Grpc::AsyncClientManager& { return client_manager; })); + EXPECT_CALL(client_manager, factoryForGrpcService(_, _, _)) + .WillOnce(Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + return std::move(client_factory); + })); + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("response"))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + NiceMock span; + if (callbacks) { + callbacks->onCreateInitialMetadata(request_headers); + callbacks->onSuccessRaw(std::move(response), span); + } +} + +TEST_P(WasmHttpFilterTest, GrpcCallBadCall) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + setupTest("grpc_call"); + setupFilter("grpc_call"); + Grpc::MockAsyncClientManager client_manager; + auto client_factory = std::make_unique(); + auto async_client = std::make_unique(); + EXPECT_CALL(*async_client, sendRaw(_, _, _, _, _, _)) + .WillOnce(Invoke([&](absl::string_view, absl::string_view, Buffer::InstancePtr&&, + Grpc::RawAsyncRequestCallbacks&, Tracing::Span&, + const Http::AsyncClient::RequestOptions&) -> Grpc::AsyncRequest* { + return nullptr; + })); + EXPECT_CALL(*client_factory, create).WillOnce(Invoke([&]() -> Grpc::RawAsyncClientPtr { + return std::move(async_client); + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillOnce(Invoke([&]() -> Grpc::AsyncClientManager& { return client_manager; })); + EXPECT_CALL(client_manager, factoryForGrpcService(_, _, _)) + .WillOnce(Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + return std::move(client_factory); + })); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); +} + +TEST_P(WasmHttpFilterTest, GrpcCallFailure) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + setupTest("grpc_call"); + setupFilter("grpc_call"); + NiceMock request; + Grpc::RawAsyncRequestCallbacks* callbacks = nullptr; + Grpc::MockAsyncClientManager client_manager; + auto client_factory = std::make_unique(); + auto async_client = std::make_unique(); + Tracing::Span* parent_span{}; + EXPECT_CALL(*async_client, sendRaw(_, _, _, _, _, _)) + .WillOnce(Invoke([&](absl::string_view service_full_name, absl::string_view method_name, + Buffer::InstancePtr&& message, Grpc::RawAsyncRequestCallbacks& cb, + Tracing::Span& span, const Http::AsyncClient::RequestOptions& options) + -> Grpc::AsyncRequest* { + EXPECT_EQ(service_full_name, "service"); + EXPECT_EQ(method_name, "method"); + ProtobufWkt::Value value; + EXPECT_TRUE(value.ParseFromArray(message->linearize(message->length()), message->length())); + EXPECT_EQ(value.string_value(), "request"); + callbacks = &cb; + parent_span = &span; + EXPECT_EQ(options.timeout->count(), 1000); + return &request; + })); + EXPECT_CALL(*client_factory, create).WillOnce(Invoke([&]() -> Grpc::RawAsyncClientPtr { + return std::move(async_client); + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillOnce(Invoke([&]() -> Grpc::AsyncClientManager& { return client_manager; })); + EXPECT_CALL(client_manager, factoryForGrpcService(_, _, _)) + .WillOnce(Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + return std::move(client_factory); + })); + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("failure bad"))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + // Test some additional error paths. + EXPECT_EQ(filter().grpcSend(99999, "", false), proxy_wasm::WasmResult::BadArgument); + EXPECT_EQ(filter().grpcSend(10000, "", false), proxy_wasm::WasmResult::NotFound); + EXPECT_EQ(filter().grpcCancel(9999), proxy_wasm::WasmResult::NotFound); + EXPECT_EQ(filter().grpcCancel(10000), proxy_wasm::WasmResult::NotFound); + EXPECT_EQ(filter().grpcClose(9999), proxy_wasm::WasmResult::NotFound); + EXPECT_EQ(filter().grpcClose(10000), proxy_wasm::WasmResult::NotFound); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + NiceMock span; + if (callbacks) { + callbacks->onFailure(Grpc::Status::WellKnownGrpcStatus::Canceled, "bad", span); + } +} + +TEST_P(WasmHttpFilterTest, GrpcCallCancel) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + setupTest("grpc_call"); + setupFilter("grpc_call"); + NiceMock request; + Grpc::RawAsyncRequestCallbacks* callbacks = nullptr; + Grpc::MockAsyncClientManager client_manager; + auto client_factory = std::make_unique(); + auto async_client = std::make_unique(); + Tracing::Span* parent_span{}; + EXPECT_CALL(*async_client, sendRaw(_, _, _, _, _, _)) + .WillOnce(Invoke([&](absl::string_view service_full_name, absl::string_view method_name, + Buffer::InstancePtr&& message, Grpc::RawAsyncRequestCallbacks& cb, + Tracing::Span& span, const Http::AsyncClient::RequestOptions& options) + -> Grpc::AsyncRequest* { + EXPECT_EQ(service_full_name, "service"); + EXPECT_EQ(method_name, "method"); + ProtobufWkt::Value value; + EXPECT_TRUE(value.ParseFromArray(message->linearize(message->length()), message->length())); + EXPECT_EQ(value.string_value(), "request"); + callbacks = &cb; + parent_span = &span; + EXPECT_EQ(options.timeout->count(), 1000); + return &request; + })); + EXPECT_CALL(*client_factory, create).WillOnce(Invoke([&]() -> Grpc::RawAsyncClientPtr { + return std::move(async_client); + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillOnce(Invoke([&]() -> Grpc::AsyncClientManager& { return client_manager; })); + EXPECT_CALL(client_manager, factoryForGrpcService(_, _, _)) + .WillOnce(Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + return std::move(client_factory); + })); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + rootContext().onQueueReady(0); +} + +TEST_P(WasmHttpFilterTest, GrpcCallClose) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + setupTest("grpc_call"); + setupFilter("grpc_call"); + NiceMock request; + Grpc::RawAsyncRequestCallbacks* callbacks = nullptr; + Grpc::MockAsyncClientManager client_manager; + auto client_factory = std::make_unique(); + auto async_client = std::make_unique(); + Tracing::Span* parent_span{}; + EXPECT_CALL(*async_client, sendRaw(_, _, _, _, _, _)) + .WillOnce(Invoke([&](absl::string_view service_full_name, absl::string_view method_name, + Buffer::InstancePtr&& message, Grpc::RawAsyncRequestCallbacks& cb, + Tracing::Span& span, const Http::AsyncClient::RequestOptions& options) + -> Grpc::AsyncRequest* { + EXPECT_EQ(service_full_name, "service"); + EXPECT_EQ(method_name, "method"); + ProtobufWkt::Value value; + EXPECT_TRUE(value.ParseFromArray(message->linearize(message->length()), message->length())); + EXPECT_EQ(value.string_value(), "request"); + callbacks = &cb; + parent_span = &span; + EXPECT_EQ(options.timeout->count(), 1000); + return &request; + })); + EXPECT_CALL(*client_factory, create).WillOnce(Invoke([&]() -> Grpc::RawAsyncClientPtr { + return std::move(async_client); + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillOnce(Invoke([&]() -> Grpc::AsyncClientManager& { return client_manager; })); + EXPECT_CALL(client_manager, factoryForGrpcService(_, _, _)) + .WillOnce(Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + return std::move(client_factory); + })); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + rootContext().onQueueReady(1); +} + +TEST_P(WasmHttpFilterTest, GrpcCallAfterDestroyed) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + setupTest("grpc_call"); + setupFilter("grpc_call"); + Grpc::MockAsyncRequest request; + Grpc::RawAsyncRequestCallbacks* callbacks = nullptr; + Grpc::MockAsyncClientManager client_manager; + auto client_factory = std::make_unique(); + auto async_client = std::make_unique(); + Tracing::Span* parent_span{}; + EXPECT_CALL(*async_client, sendRaw(_, _, _, _, _, _)) + .WillOnce(Invoke([&](absl::string_view service_full_name, absl::string_view method_name, + Buffer::InstancePtr&& message, Grpc::RawAsyncRequestCallbacks& cb, + Tracing::Span& span, const Http::AsyncClient::RequestOptions& options) + -> Grpc::AsyncRequest* { + EXPECT_EQ(service_full_name, "service"); + EXPECT_EQ(method_name, "method"); + ProtobufWkt::Value value; + EXPECT_TRUE(value.ParseFromArray(message->linearize(message->length()), message->length())); + EXPECT_EQ(value.string_value(), "request"); + callbacks = &cb; + parent_span = &span; + EXPECT_EQ(options.timeout->count(), 1000); + return &request; + })); + EXPECT_CALL(*client_factory, create).WillOnce(Invoke([&]() -> Grpc::RawAsyncClientPtr { + return std::move(async_client); + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillOnce(Invoke([&]() -> Grpc::AsyncClientManager& { return client_manager; })); + EXPECT_CALL(client_manager, factoryForGrpcService(_, _, _)) + .WillOnce(Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + return std::move(client_factory); + })); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + EXPECT_CALL(request, cancel()).WillOnce([&]() { callbacks = nullptr; }); + + // Destroy the Context, Plugin and VM. + context_.reset(); + plugin_.reset(); + wasm_.reset(); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_EQ(callbacks, nullptr); + NiceMock span; + if (callbacks) { + callbacks->onSuccessRaw(std::move(response), span); + } +} + +void WasmHttpFilterTest::setupGrpcStreamTest(Grpc::RawAsyncStreamCallbacks*& callbacks) { + setupTest("grpc_stream"); + setupFilter("grpc_stream"); + + EXPECT_CALL(async_client_manager_, factoryForGrpcService(_, _, _)) + .WillRepeatedly( + Invoke([&](const GrpcService&, Stats::Scope&, bool) -> Grpc::AsyncClientFactoryPtr { + auto client_factory = std::make_unique(); + EXPECT_CALL(*client_factory, create) + .WillRepeatedly(Invoke([&]() -> Grpc::RawAsyncClientPtr { + auto async_client = std::make_unique(); + EXPECT_CALL(*async_client, startRaw(_, _, _, _)) + .WillRepeatedly(Invoke( + [&](absl::string_view service_full_name, absl::string_view method_name, + Grpc::RawAsyncStreamCallbacks& cb, + const Http::AsyncClient::StreamOptions&) -> Grpc::RawAsyncStream* { + EXPECT_EQ(service_full_name, "service"); + if (method_name != "method") { + return nullptr; + } + callbacks = &cb; + return &async_stream_; + })); + return async_client; + })); + return client_factory; + })); + EXPECT_CALL(cluster_manager_, grpcAsyncClientManager()) + .WillRepeatedly(Invoke([&]() -> Grpc::AsyncClientManager& { return async_client_manager_; })); +} + +TEST_P(WasmHttpFilterTest, GrpcStream) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + Grpc::RawAsyncStreamCallbacks* callbacks = nullptr; + setupGrpcStreamTest(callbacks); + + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("response response"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("close done"))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + if (callbacks) { + Http::TestRequestHeaderMapImpl create_initial_metadata{{"test", "create_initial_metadata"}}; + callbacks->onCreateInitialMetadata(create_initial_metadata); + callbacks->onReceiveInitialMetadata(std::make_unique()); + callbacks->onReceiveMessageRaw(std::move(response)); + callbacks->onReceiveTrailingMetadata(std::make_unique()); + callbacks->onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Ok, "done"); + } +} + +// Local close followed by remote close. +TEST_P(WasmHttpFilterTest, GrpcStreamCloseLocal) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + Grpc::RawAsyncStreamCallbacks* callbacks = nullptr; + setupGrpcStreamTest(callbacks); + + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("response close"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("close ok"))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + ProtobufWkt::Value value; + value.set_string_value("close"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + if (callbacks) { + Http::TestRequestHeaderMapImpl create_initial_metadata{{"test", "create_initial_metadata"}}; + callbacks->onCreateInitialMetadata(create_initial_metadata); + callbacks->onReceiveInitialMetadata(std::make_unique()); + callbacks->onReceiveMessageRaw(std::move(response)); + callbacks->onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Ok, "ok"); + } +} + +// Remote close followed by local close. +TEST_P(WasmHttpFilterTest, GrpcStreamCloseRemote) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + Grpc::RawAsyncStreamCallbacks* callbacks = nullptr; + setupGrpcStreamTest(callbacks); + + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("response response"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("close close"))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + if (callbacks) { + Http::TestRequestHeaderMapImpl create_initial_metadata{{"test", "create_initial_metadata"}}; + callbacks->onCreateInitialMetadata(create_initial_metadata); + callbacks->onReceiveInitialMetadata(std::make_unique()); + callbacks->onReceiveMessageRaw(std::move(response)); + callbacks->onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Ok, "close"); + } +} + +TEST_P(WasmHttpFilterTest, GrpcStreamCancel) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + Grpc::RawAsyncStreamCallbacks* callbacks = nullptr; + setupGrpcStreamTest(callbacks); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + NiceMock span; + if (callbacks) { + Http::TestRequestHeaderMapImpl create_initial_metadata{{"test", "create_initial_metadata"}}; + callbacks->onCreateInitialMetadata(create_initial_metadata); + callbacks->onReceiveInitialMetadata(std::make_unique( + Http::TestResponseHeaderMapImpl{{"test", "reset"}})); + } +} + +TEST_P(WasmHttpFilterTest, GrpcStreamOpenAtShutdown) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): gRPC call outs not yet supported in the Rust SDK. + return; + } + Grpc::RawAsyncStreamCallbacks* callbacks = nullptr; + setupGrpcStreamTest(callbacks); + + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq("response response"))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter().decodeHeaders(request_headers, false)); + + ProtobufWkt::Value value; + value.set_string_value("response"); + std::string response_string; + EXPECT_TRUE(value.SerializeToString(&response_string)); + auto response = std::make_unique(response_string); + EXPECT_NE(callbacks, nullptr); + NiceMock span; + if (callbacks) { + Http::TestRequestHeaderMapImpl create_initial_metadata{{"test", "create_initial_metadata"}}; + callbacks->onCreateInitialMetadata(create_initial_metadata); + callbacks->onReceiveInitialMetadata(std::make_unique()); + callbacks->onReceiveMessageRaw(std::move(response)); + callbacks->onReceiveTrailingMetadata(std::make_unique()); + } + + // Destroy the Context, Plugin and VM. + context_.reset(); + plugin_.reset(); + wasm_.reset(); +} + +// Test metadata access including CEL expressions. +// TODO: re-enable this on Windows if and when the CEL `Antlr` parser compiles on Windows. +#if defined(ENVOY_WASM_V8) || defined(ENVOY_WASM_WAVM) +TEST_P(WasmHttpFilterTest, Metadata) { + setupTest("", "metadata"); + setupFilter(); + envoy::config::core::v3::Node node_data; + ProtobufWkt::Value node_val; + node_val.set_string_value("wasm_node_get_value"); + (*node_data.mutable_metadata()->mutable_fields())["wasm_node_get_key"] = node_val; + EXPECT_CALL(local_info_, node()).WillRepeatedly(ReturnRef(node_data)); + EXPECT_CALL(rootContext(), + log_(spdlog::level::debug, Eq(absl::string_view("onTick wasm_node_get_value")))); + + EXPECT_CALL(filter(), + log_(spdlog::level::err, Eq(absl::string_view("onBody wasm_node_get_value")))); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("header path /")))); + EXPECT_CALL(filter(), + log_(spdlog::level::trace, + Eq(absl::string_view("Struct wasm_request_get_value wasm_request_get_value")))); + if (std::get<1>(GetParam()) != "rust") { + // TODO(PiotrSikora): not yet supported in the Rust SDK. + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("server is envoy-wasm")))); + } + + request_stream_info_.metadata_.mutable_filter_metadata()->insert( + Protobuf::MapPair( + HttpFilters::HttpFilterNames::get().Wasm, + MessageUtil::keyValueStruct("wasm_request_get_key", "wasm_request_get_value"))); + + rootContext().onTick(0); + + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + absl::optional dur = std::chrono::nanoseconds(15000000); + EXPECT_CALL(request_stream_info_, requestComplete()).WillRepeatedly(Return(dur)); + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("duration is 15000000")))); + if (std::get<1>(GetParam()) != "rust") { + // TODO(PiotrSikora): not yet supported in the Rust SDK. + EXPECT_CALL(filter(), log_(spdlog::level::info, Eq(absl::string_view("grpc service: test")))); + } + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}, {"biz", "baz"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, false)); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter().decodeData(data, true)); + + StreamInfo::MockStreamInfo log_stream_info; + filter().log(&request_headers, nullptr, nullptr, log_stream_info); + + const auto& result = request_stream_info_.filterState()->getDataReadOnly( + "wasm.wasm_request_set_key"); + EXPECT_EQ("wasm_request_set_value", result.value()); + + filter().onDestroy(); + filter().onDestroy(); // Does nothing. +} +#endif + +TEST_P(WasmHttpFilterTest, Property) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): test not yet implemented using Rust SDK. + return; + } + setupTest("", "property"); + setupFilter(); + envoy::config::core::v3::Node node_data; + ProtobufWkt::Value node_val; + node_val.set_string_value("sample_data"); + (*node_data.mutable_metadata()->mutable_fields())["istio.io/metadata"] = node_val; + EXPECT_CALL(local_info_, node()).WillRepeatedly(ReturnRef(node_data)); + + request_stream_info_.metadata_.mutable_filter_metadata()->insert( + Protobuf::MapPair( + HttpFilters::HttpFilterNames::get().Wasm, + MessageUtil::keyValueStruct("wasm_request_get_key", "wasm_request_get_value"))); + EXPECT_CALL(request_stream_info_, responseCode()).WillRepeatedly(Return(403)); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(request_stream_info_)); + + // test outputs should match inputs + EXPECT_CALL(filter(), + log_(spdlog::level::warn, Eq(absl::string_view("request.path: /test_context")))); + EXPECT_CALL(filter(), + log_(spdlog::level::warn, Eq(absl::string_view("node.metadata: sample_data")))); + EXPECT_CALL(filter(), + log_(spdlog::level::warn, Eq(absl::string_view("metadata: wasm_request_get_value")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("response.code: 403")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, Eq(absl::string_view("state: wasm_value")))); + + root_context_->onTick(0); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/test_context"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); + StreamInfo::MockStreamInfo log_stream_info; + request_stream_info_.route_name_ = "route12"; + request_stream_info_.requested_server_name_ = "w3.org"; + NiceMock connection; + EXPECT_CALL(connection, id()).WillRepeatedly(Return(4)); + EXPECT_CALL(encoder_callbacks_, connection()).WillRepeatedly(Return(&connection)); + NiceMock route_entry; + EXPECT_CALL(request_stream_info_, routeEntry()).WillRepeatedly(Return(&route_entry)); + filter().log(&request_headers, nullptr, nullptr, log_stream_info); +} + +TEST_P(WasmHttpFilterTest, SharedData) { + setupTest("shared_data"); + EXPECT_CALL(rootContext(), log_(spdlog::level::info, Eq(absl::string_view("set CasMismatch")))); + EXPECT_CALL(rootContext(), + log_(spdlog::level::debug, Eq(absl::string_view("get 1 shared_data_value1")))); + if (std::get<1>(GetParam()) == "rust") { + EXPECT_CALL(rootContext(), + log_(spdlog::level::warn, Eq(absl::string_view("get 2 shared_data_value2")))); + } else { + EXPECT_CALL(rootContext(), + log_(spdlog::level::critical, Eq(absl::string_view("get 2 shared_data_value2")))); + } + EXPECT_CALL(rootContext(), + log_(spdlog::level::debug, Eq(absl::string_view("get of bad key not found")))); + EXPECT_CALL(rootContext(), + log_(spdlog::level::debug, Eq(absl::string_view("second get of bad key not found")))); + rootContext().onTick(0); + rootContext().onQueueReady(0); +} + +TEST_P(WasmHttpFilterTest, SharedQueue) { + setupTest("shared_queue"); + setupFilter("shared_queue"); + EXPECT_CALL(filter(), + log_(spdlog::level::warn, Eq(absl::string_view("onRequestHeaders enqueue Ok")))); + EXPECT_CALL(filter(), log_(spdlog::level::warn, + Eq(absl::string_view("onRequestHeaders not found bad_shared_queue")))); + EXPECT_CALL(rootContext(), + log_(spdlog::level::warn, Eq(absl::string_view("onQueueReady bad token not found")))) + .Times(2); + EXPECT_CALL(rootContext(), + log_(spdlog::level::warn, Eq(absl::string_view("onQueueReady extra data not found")))) + .Times(2); + EXPECT_CALL(rootContext(), log_(spdlog::level::info, Eq(absl::string_view("onQueueReady")))) + .Times(2); + EXPECT_CALL(rootContext(), log_(spdlog::level::debug, Eq(absl::string_view("data data1 Ok")))); + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); + auto token = proxy_wasm::resolveQueueForTest("vm_id", "my_shared_queue"); + root_context_->onQueueReady(token); +} + +// Script using a root_id which is not registered. +TEST_P(WasmHttpFilterTest, RootIdNotRegistered) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): proxy_get_property("root_id") is not yet supported in the Rust SDK. + return; + } + setupTest(); + setupFilter(); + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); +} + +// Script using an explicit root_id which is registered. +TEST_P(WasmHttpFilterTest, RootId1) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): proxy_get_property("root_id") is not yet supported in the Rust SDK. + return; + } + setupTest("context1"); + setupFilter("context1"); + EXPECT_CALL(filter(), log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders1 2")))); + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); +} + +// Script using an explicit root_id which is registered. +TEST_P(WasmHttpFilterTest, RootId2) { + if (std::get<1>(GetParam()) == "rust") { + // TODO(PiotrSikora): proxy_get_property("root_id") is not yet supported in the Rust SDK. + return; + } + setupTest("context2"); + setupFilter("context2"); + EXPECT_CALL(filter(), log_(spdlog::level::debug, Eq(absl::string_view("onRequestHeaders2 2")))); + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter().decodeHeaders(request_headers, true)); +} + +} // namespace Wasm +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/wasm/BUILD b/test/extensions/filters/network/wasm/BUILD new file mode 100644 index 000000000000..d21eba6c0853 --- /dev/null +++ b/test/extensions/filters/network/wasm/BUILD @@ -0,0 +1,54 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/filters/network/wasm/test_data:test_cpp.wasm", + ]), + extension_name = "envoy.filters.network.wasm", + deps = [ + "//source/common/common:base64_lib", + "//source/common/common:hex_lib", + "//source/common/crypto:utility_lib", + "//source/extensions/common/crypto:utility_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/filters/network/wasm:config", + "//test/extensions/filters/network/wasm/test_data:test_cpp_plugin", + "//test/mocks/server:server_mocks", + "//test/test_common:environment_lib", + "@envoy_api//envoy/extensions/filters/network/wasm/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "wasm_filter_test", + srcs = ["wasm_filter_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/filters/network/wasm/test_data:logging_rust.wasm", + "//test/extensions/filters/network/wasm/test_data:test_cpp.wasm", + ]), + extension_name = "envoy.filters.network.wasm", + deps = [ + "//source/extensions/filters/network/wasm:wasm_filter_lib", + "//test/extensions/filters/network/wasm/test_data:test_cpp_plugin", + "//test/mocks/network:network_mocks", + "//test/mocks/server:server_mocks", + "//test/test_common:wasm_lib", + ], +) diff --git a/test/extensions/filters/network/wasm/config_test.cc b/test/extensions/filters/network/wasm/config_test.cc new file mode 100644 index 000000000000..58d17c177fb7 --- /dev/null +++ b/test/extensions/filters/network/wasm/config_test.cc @@ -0,0 +1,190 @@ +#include "envoy/extensions/filters/network/wasm/v3/wasm.pb.validate.h" + +#include "common/common/base64.h" +#include "common/common/hex.h" +#include "common/crypto/utility.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/network/wasm/config.h" +#include "extensions/filters/network/wasm/wasm_filter.h" + +#include "test/mocks/server/mocks.h" +#include "test/test_common/environment.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace Wasm { + +class WasmNetworkFilterConfigTest : public testing::TestWithParam { +protected: + WasmNetworkFilterConfigTest() : api_(Api::createApiForTest(stats_store_)) { + ON_CALL(context_, api()).WillByDefault(ReturnRef(*api_)); + ON_CALL(context_, scope()).WillByDefault(ReturnRef(stats_store_)); + ON_CALL(context_, listenerMetadata()).WillByDefault(ReturnRef(listener_metadata_)); + ON_CALL(context_, initManager()).WillByDefault(ReturnRef(init_manager_)); + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + ON_CALL(context_, dispatcher()).WillByDefault(ReturnRef(dispatcher_)); + } + + void SetUp() override { Envoy::Extensions::Common::Wasm::clearCodeCacheForTesting(); } + + void initializeForRemote() { + retry_timer_ = new Event::MockTimer(); + + EXPECT_CALL(dispatcher_, createTimer_(_)).WillOnce(Invoke([this](Event::TimerCb timer_cb) { + retry_timer_cb_ = timer_cb; + return retry_timer_; + })); + } + + NiceMock context_; + Stats::IsolatedStoreImpl stats_store_; + 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_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmNetworkFilterConfigTest, testing_values); + +TEST_P(WasmNetworkFilterConfigTest, YamlLoadFromFileWasm) { + if (GetParam() == "null") { + return; + } + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: + filename: "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/test_cpp.wasm" + )EOF")); + + envoy::extensions::filters::network::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Network::MockConnection connection; + EXPECT_CALL(connection, addFilter(_)); + cb(connection); +} + +TEST_P(WasmNetworkFilterConfigTest, YamlLoadInlineWasm) { + const std::string code = + GetParam() != "null" + ? TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/test_cpp.wasm")) + : "NetworkTestCpp"; + EXPECT_FALSE(code.empty()); + const std::string yaml = absl::StrCat(R"EOF( + config: + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: { inline_bytes: ")EOF", + Base64::encode(code.data(), code.size()), R"EOF(" } + )EOF"); + + envoy::extensions::filters::network::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context_); + EXPECT_CALL(init_watcher_, ready()); + context_.initManager().initialize(init_watcher_); + EXPECT_EQ(context_.initManager().state(), Init::Manager::State::Initialized); + Network::MockConnection connection; + EXPECT_CALL(connection, addFilter(_)); + cb(connection); +} + +TEST_P(WasmNetworkFilterConfigTest, YamlLoadInlineBadCode) { + const std::string yaml = absl::StrCat(R"EOF( + config: + name: "test" + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: { inline_string: "bad code" } + )EOF"); + + envoy::extensions::filters::network::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, context_), + Extensions::Common::Wasm::WasmException, + "Unable to create Wasm network filter test"); +} + +TEST_P(WasmNetworkFilterConfigTest, YamlLoadInlineBadCodeFailOpenNackConfig) { + const std::string yaml = absl::StrCat(R"EOF( + config: + name: "test" + fail_open: true + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: { inline_string: "bad code" } + )EOF"); + + envoy::extensions::filters::network::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + WasmFilterConfig factory; + EXPECT_THROW_WITH_MESSAGE(factory.createFilterFactoryFromProto(proto_config, context_), + Extensions::Common::Wasm::WasmException, + "Unable to create Wasm network filter test"); +} + +TEST_P(WasmNetworkFilterConfigTest, FilterConfigFailOpen) { + if (GetParam() == "null") { + return; + } + const std::string yaml = TestEnvironment::substitute(absl::StrCat(R"EOF( + config: + fail_open: true + vm_config: + runtime: "envoy.wasm.runtime.)EOF", + GetParam(), R"EOF(" + code: + local: + filename: "{{ test_rundir }}/test/extensions/filters/network/wasm/test_data/test_cpp.wasm" + )EOF")); + + envoy::extensions::filters::network::wasm::v3::Wasm proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + NetworkFilters::Wasm::FilterConfig filter_config(proto_config, context_); + filter_config.wasm()->fail(proxy_wasm::FailState::RuntimeError, ""); + EXPECT_EQ(filter_config.createFilter(), nullptr); +} + +} // namespace Wasm +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/wasm/test_data/BUILD b/test/extensions/filters/network/wasm/test_data/BUILD new file mode 100644 index 000000000000..a33d53447d03 --- /dev/null +++ b/test/extensions/filters/network/wasm/test_data/BUILD @@ -0,0 +1,44 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary", "wasm_rust_binary") + +licenses(["notice"]) # Apache 2 + +envoy_package() + +wasm_rust_binary( + name = "logging_rust.wasm", + srcs = ["logging_rust/src/lib.rs"], + deps = [ + "//bazel/external/cargo:log", + "//bazel/external/cargo:proxy_wasm", + ], +) + +envoy_cc_library( + name = "test_cpp_plugin", + srcs = [ + "test_cpp.cc", + "test_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//external:abseil_node_hash_map", + "//source/common/common:assert_lib", + "//source/common/common:c_smart_ptr_lib", + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_wasm_cc_binary( + name = "test_cpp.wasm", + srcs = ["test_cpp.cc"], + deps = [ + "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics", + ], +) diff --git a/test/extensions/filters/network/wasm/test_data/logging_rust/Cargo.toml b/test/extensions/filters/network/wasm/test_data/logging_rust/Cargo.toml new file mode 100644 index 000000000000..a82aed3df58d --- /dev/null +++ b/test/extensions/filters/network/wasm/test_data/logging_rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +description = "Proxy-Wasm logging test" +name = "logging_rust" +version = "0.0.1" +authors = ["Piotr Sikora "] +edition = "2018" + +[dependencies] +proxy-wasm = "0.1" +log = "0.4" + +[lib] +crate-type = ["cdylib"] +path = "src/*.rs" + +[profile.release] +lto = true +opt-level = 3 +panic = "abort" + +[raze] +workspace_path = "//bazel/external/cargo" +genmode = "Remote" + +[raze.crates.log.'0.4.11'] +additional_flags = ["--cfg=atomic_cas"] diff --git a/test/extensions/filters/network/wasm/test_data/logging_rust/src/lib.rs b/test/extensions/filters/network/wasm/test_data/logging_rust/src/lib.rs new file mode 100644 index 000000000000..d03230863f29 --- /dev/null +++ b/test/extensions/filters/network/wasm/test_data/logging_rust/src/lib.rs @@ -0,0 +1,68 @@ +use log::trace; +use proxy_wasm::hostcalls; +use proxy_wasm::traits::{Context, StreamContext}; +use proxy_wasm::types::*; + +#[no_mangle] +pub fn _start() { + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_stream_context(|context_id, _| -> Box { + Box::new(TestStream { context_id }) + }); +} + +struct TestStream { + context_id: u32, +} + +impl Context for TestStream {} + +impl StreamContext for TestStream { + fn on_new_connection(&mut self) -> Action { + trace!("onNewConnection {}", self.context_id); + Action::Continue + } + + fn on_downstream_data(&mut self, data_size: usize, end_of_stream: bool) -> Action { + if let Some(data) = self.get_downstream_data(0, data_size) { + trace!( + "onDownstreamData {} len={} end_stream={}\n{}", + self.context_id, + data_size, + end_of_stream as u32, + String::from_utf8(data).unwrap() + ); + } + hostcalls::set_buffer(BufferType::DownstreamData, 0, data_size, b"write").unwrap(); + Action::Continue + } + + fn on_upstream_data(&mut self, data_size: usize, end_of_stream: bool) -> Action { + if let Some(data) = self.get_upstream_data(0, data_size) { + trace!( + "onUpstreamData {} len={} end_stream={}\n{}", + self.context_id, + data_size, + end_of_stream as u32, + String::from_utf8(data).unwrap() + ); + } + Action::Continue + } + + fn on_downstream_close(&mut self, peer_type: PeerType) { + trace!( + "onDownstreamConnectionClose {} {}", + self.context_id, + peer_type as u32, + ); + } + + fn on_upstream_close(&mut self, peer_type: PeerType) { + trace!( + "onUpstreamConnectionClose {} {}", + self.context_id, + peer_type as u32, + ); + } +} diff --git a/test/extensions/filters/network/wasm/test_data/test_cpp.cc b/test/extensions/filters/network/wasm/test_data/test_cpp.cc new file mode 100644 index 000000000000..644b52eb6174 --- /dev/null +++ b/test/extensions/filters/network/wasm/test_data/test_cpp.cc @@ -0,0 +1,63 @@ +// NOLINT(namespace-envoy) +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#else +#include "include/proxy-wasm/null_plugin.h" +#endif + +START_WASM_PLUGIN(NetworkTestCpp) + +static int* badptr = nullptr; + +class ExampleContext : public Context { +public: + explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterStatus onNewConnection() override; + FilterStatus onDownstreamData(size_t data_length, bool end_stream) override; + FilterStatus onUpstreamData(size_t data_length, bool end_stream) override; + void onForeignFunction(uint32_t, uint32_t) override; + void onDownstreamConnectionClose(CloseType close_type) override; + void onUpstreamConnectionClose(CloseType close_type) override; +}; +static RegisterContextFactory register_ExampleContext(CONTEXT_FACTORY(ExampleContext)); + +FilterStatus ExampleContext::onNewConnection() { + logTrace("onNewConnection " + std::to_string(id())); + return FilterStatus::Continue; +} + +FilterStatus ExampleContext::onDownstreamData(size_t data_length, bool end_stream) { + WasmDataPtr data = getBufferBytes(WasmBufferType::NetworkDownstreamData, 0, data_length); + logTrace("onDownstreamData " + std::to_string(id()) + " len=" + std::to_string(data_length) + + " end_stream=" + std::to_string(end_stream) + "\n" + std::string(data->view())); + setBuffer(WasmBufferType::NetworkDownstreamData, 0, 5, "write"); + return FilterStatus::Continue; +} + +FilterStatus ExampleContext::onUpstreamData(size_t data_length, bool end_stream) { + WasmDataPtr data = getBufferBytes(WasmBufferType::NetworkUpstreamData, 0, data_length); + logTrace("onUpstreamData " + std::to_string(id()) + " len=" + std::to_string(data_length) + + " end_stream=" + std::to_string(end_stream) + "\n" + std::string(data->view())); + return FilterStatus::Continue; +} + +void ExampleContext::onForeignFunction(uint32_t, uint32_t) { + logTrace("before segv"); + *badptr = 1; + logTrace("after segv"); +} + +void ExampleContext::onDownstreamConnectionClose(CloseType close_type) { + logTrace("onDownstreamConnectionClose " + std::to_string(id()) + " " + + std::to_string(static_cast(close_type))); +} + +void ExampleContext::onUpstreamConnectionClose(CloseType close_type) { + logTrace("onUpstreamConnectionClose " + std::to_string(id()) + " " + + std::to_string(static_cast(close_type))); +} + +END_WASM_PLUGIN diff --git a/test/extensions/filters/network/wasm/test_data/test_cpp_null_plugin.cc b/test/extensions/filters/network/wasm/test_data/test_cpp_null_plugin.cc new file mode 100644 index 000000000000..d626a15f607e --- /dev/null +++ b/test/extensions/filters/network/wasm/test_data/test_cpp_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace NetworkTestCpp { +NullPluginRegistry* context_registry_; +} // namespace NetworkTestCpp + +RegisterNullVmPluginFactory register_common_wasm_test_cpp_plugin("NetworkTestCpp", []() { + return std::make_unique(NetworkTestCpp::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/filters/network/wasm/wasm_filter_test.cc b/test/extensions/filters/network/wasm/wasm_filter_test.cc new file mode 100644 index 000000000000..6bf1ca8151e6 --- /dev/null +++ b/test/extensions/filters/network/wasm/wasm_filter_test.cc @@ -0,0 +1,208 @@ +#include "envoy/server/lifecycle_notifier.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/filters/network/wasm/wasm_filter.h" + +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/test_common/wasm_base.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Eq; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace Wasm { + +using Envoy::Extensions::Common::Wasm::Context; +using Envoy::Extensions::Common::Wasm::Plugin; +using Envoy::Extensions::Common::Wasm::PluginSharedPtr; +using Envoy::Extensions::Common::Wasm::Wasm; +using proxy_wasm::ContextBase; + +class TestFilter : public Context { +public: + TestFilter(Wasm* wasm, uint32_t root_context_id, PluginSharedPtr plugin) + : Context(wasm, root_context_id, plugin) {} + MOCK_CONTEXT_LOG_; + + void testClose() { onCloseTCP(); } +}; + +class TestRoot : public Context { +public: + TestRoot(Wasm* wasm, const std::shared_ptr& plugin) : Context(wasm, plugin) {} + MOCK_CONTEXT_LOG_; +}; + +class WasmNetworkFilterTest : public Common::Wasm::WasmNetworkFilterTestBase< + testing::TestWithParam>> { +public: + WasmNetworkFilterTest() = default; + ~WasmNetworkFilterTest() override = default; + + void setupConfig(const std::string& code, std::string vm_configuration, bool fail_open = false) { + if (code.empty()) { + setupWasmCode(vm_configuration); + } else { + code_ = code; + } + setupBase( + std::get<0>(GetParam()), code_, + [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + return new TestRoot(wasm, plugin); + }, + "" /* root_id */, "" /* vm_configuration */, fail_open); + } + + void setupFilter() { setupFilterBase(""); } + + TestFilter& filter() { return *static_cast(context_.get()); } + +private: + void setupWasmCode(std::string vm_configuration) { + if (std::get<0>(GetParam()) == "null") { + code_ = "NetworkTestCpp"; + } else { + if (std::get<1>(GetParam()) == "cpp") { + code_ = TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath( + "test/extensions/filters/network/wasm/test_data/test_cpp.wasm")); + } else { + code_ = TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath(absl::StrCat( + "test/extensions/filters/network/wasm/test_data/", vm_configuration + "_rust.wasm"))); + } + } + EXPECT_FALSE(code_.empty()); + } + +protected: + std::string code_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + std::make_tuple("v8", "cpp"), std::make_tuple("v8", "rust"), +#endif +#if defined(ENVOY_WASM_WAVM) + std::make_tuple("wavm", "cpp"), std::make_tuple("wavm", "rust"), +#endif + std::make_tuple("null", "cpp")); +INSTANTIATE_TEST_SUITE_P(RuntimesAndLanguages, WasmNetworkFilterTest, testing_values); + +// Bad code in initial config. +TEST_P(WasmNetworkFilterTest, BadCode) { + setupConfig("bad code", ""); + EXPECT_EQ(wasm_, nullptr); + setupFilter(); + filter().isFailed(); + EXPECT_CALL(read_filter_callbacks_.connection_, + close(Envoy::Network::ConnectionCloseType::FlushWrite)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter().onNewConnection()); +} + +TEST_P(WasmNetworkFilterTest, BadCodeFailOpen) { + setupConfig("bad code", "", true); + EXPECT_EQ(wasm_, nullptr); + setupFilter(); + filter().isFailed(); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onNewConnection()); +} + +// Test happy path. +TEST_P(WasmNetworkFilterTest, HappyPath) { + setupConfig("", "logging"); + setupFilter(); + + EXPECT_CALL(filter(), log_(spdlog::level::trace, Eq(absl::string_view("onNewConnection 2")))); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onNewConnection()); + + Buffer::OwnedImpl fake_downstream_data("Fake"); + EXPECT_CALL(filter(), log_(spdlog::level::trace, + Eq(absl::string_view("onDownstreamData 2 len=4 end_stream=0\nFake")))); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onData(fake_downstream_data, false)); + EXPECT_EQ(fake_downstream_data.toString(), "write"); + + Buffer::OwnedImpl fake_upstream_data("Done"); + EXPECT_CALL(filter(), log_(spdlog::level::trace, + Eq(absl::string_view("onUpstreamData 2 len=4 end_stream=1\nDone")))); + EXPECT_CALL(filter(), + log_(spdlog::level::trace, Eq(absl::string_view("onUpstreamConnectionClose 2 0")))); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onWrite(fake_upstream_data, true)); + filter().onAboveWriteBufferHighWatermark(); + filter().onBelowWriteBufferLowWatermark(); + + EXPECT_CALL(filter(), + log_(spdlog::level::trace, Eq(absl::string_view("onDownstreamConnectionClose 2 1")))); + read_filter_callbacks_.connection_.close(Network::ConnectionCloseType::FlushWrite); + // Noop. + read_filter_callbacks_.connection_.close(Network::ConnectionCloseType::FlushWrite); + filter().testClose(); +} + +TEST_P(WasmNetworkFilterTest, CloseDownstreamFirst) { + setupConfig("", "logging"); + setupFilter(); + + EXPECT_CALL(filter(), log_(spdlog::level::trace, Eq(absl::string_view("onNewConnection 2")))); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onNewConnection()); + + EXPECT_CALL(filter(), + log_(spdlog::level::trace, Eq(absl::string_view("onDownstreamConnectionClose 2 1")))); + write_filter_callbacks_.connection_.close(Network::ConnectionCloseType::FlushWrite); + read_filter_callbacks_.connection_.close(Network::ConnectionCloseType::FlushWrite); +} + +TEST_P(WasmNetworkFilterTest, CloseStream) { + setupConfig("", "logging"); + setupFilter(); + + // No Context, does nothing. + filter().onEvent(Network::ConnectionEvent::RemoteClose); + Buffer::OwnedImpl fake_upstream_data("Done"); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onWrite(fake_upstream_data, true)); + Buffer::OwnedImpl fake_downstream_data("Fake"); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onData(fake_downstream_data, false)); + + // Create context. + EXPECT_CALL(filter(), log_(spdlog::level::trace, Eq(absl::string_view("onNewConnection 2")))); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onNewConnection()); + EXPECT_CALL(filter(), + log_(spdlog::level::trace, Eq(absl::string_view("onDownstreamConnectionClose 2 1")))); + EXPECT_CALL(filter(), + log_(spdlog::level::trace, Eq(absl::string_view("onDownstreamConnectionClose 2 2")))); + + filter().onEvent(static_cast(9999)); // Does nothing. + filter().onEvent(Network::ConnectionEvent::RemoteClose); + filter().closeStream(proxy_wasm::WasmStreamType::Downstream); + filter().closeStream(proxy_wasm::WasmStreamType::Upstream); +} + +TEST_P(WasmNetworkFilterTest, SegvFailOpen) { + if (std::get<0>(GetParam()) != "v8" || std::get<1>(GetParam()) != "cpp") { + return; + } + setupConfig("", "logging", true); + EXPECT_TRUE(plugin_->fail_open_); + setupFilter(); + + EXPECT_CALL(filter(), log_(spdlog::level::trace, Eq(absl::string_view("onNewConnection 2")))); + EXPECT_EQ(Network::FilterStatus::Continue, filter().onNewConnection()); + + EXPECT_CALL(filter(), log_(spdlog::level::trace, Eq(absl::string_view("before segv")))); + filter().onForeignFunction(0, 0); + EXPECT_TRUE(wasm_->wasm()->isFailed()); + + Buffer::OwnedImpl fake_downstream_data("Fake"); + // No logging expected. + EXPECT_EQ(Network::FilterStatus::Continue, filter().onData(fake_downstream_data, false)); +} + +} // namespace Wasm +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/stats_sinks/wasm/BUILD b/test/extensions/stats_sinks/wasm/BUILD new file mode 100644 index 000000000000..6135c8cfcf0a --- /dev/null +++ b/test/extensions/stats_sinks/wasm/BUILD @@ -0,0 +1,48 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/stats_sinks/wasm/test_data:test_context_cpp.wasm", + ]), + extension_name = "envoy.stat_sinks.wasm", + deps = [ + "//source/extensions/stat_sinks/wasm:config", + "//test/extensions/stats_sinks/wasm/test_data:test_context_cpp_plugin", + "//test/mocks/server:server_mocks", + "@envoy_api//envoy/extensions/stat_sinks/wasm/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "wasm_stat_sink_test", + srcs = ["wasm_stat_sink_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/stats_sinks/wasm/test_data:test_context_cpp.wasm", + ]), + extension_name = "envoy.stat_sinks.wasm", + external_deps = ["abseil_optional"], + deps = [ + "//source/common/stats:stats_lib", + "//source/extensions/common/wasm:wasm_lib", + "//test/extensions/stats_sinks/wasm/test_data:test_context_cpp_plugin", + "//test/mocks/stats:stats_mocks", + "//test/test_common:wasm_lib", + ], +) diff --git a/test/extensions/stats_sinks/wasm/config_test.cc b/test/extensions/stats_sinks/wasm/config_test.cc new file mode 100644 index 000000000000..1e115dd2f946 --- /dev/null +++ b/test/extensions/stats_sinks/wasm/config_test.cc @@ -0,0 +1,106 @@ +#include "envoy/extensions/stat_sinks/wasm/v3/wasm.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/protobuf/protobuf.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/stat_sinks/wasm/config.h" +#include "extensions/stat_sinks/wasm/wasm_stat_sink_impl.h" +#include "extensions/stat_sinks/well_known_names.h" + +#include "test/mocks/server/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace Wasm { + +class WasmStatSinkConfigTest : public testing::TestWithParam { +protected: + WasmStatSinkConfigTest() { + config_.mutable_config()->mutable_vm_config()->set_runtime( + absl::StrCat("envoy.wasm.runtime.", GetParam())); + if (GetParam() != "null") { + config_.mutable_config()->mutable_vm_config()->mutable_code()->mutable_local()->set_filename( + TestEnvironment::substitute( + "{{ test_rundir " + "}}/test/extensions/stats_sinks/wasm/test_data/test_context_cpp.wasm")); + } else { + config_.mutable_config() + ->mutable_vm_config() + ->mutable_code() + ->mutable_local() + ->set_inline_bytes("CommonWasmTestContextCpp"); + } + config_.mutable_config()->set_name("test"); + } + + void initializeWithConfig(const envoy::extensions::stat_sinks::wasm::v3::Wasm& config) { + auto factory = Registry::FactoryRegistry::getFactory( + StatsSinkNames::get().Wasm); + ASSERT_NE(factory, nullptr); + api_ = Api::createApiForTest(stats_store_); + EXPECT_CALL(context_, api()).WillRepeatedly(testing::ReturnRef(*api_)); + EXPECT_CALL(context_, initManager()).WillRepeatedly(testing::ReturnRef(init_manager_)); + EXPECT_CALL(context_, lifecycleNotifier()) + .WillRepeatedly(testing::ReturnRef(lifecycle_notifier_)); + sink_ = factory->createStatsSink(config, context_); + EXPECT_CALL(init_watcher_, ready()); + init_manager_.initialize(init_watcher_); + } + + envoy::extensions::stat_sinks::wasm::v3::Wasm config_; + testing::NiceMock context_; + testing::NiceMock lifecycle_notifier_; + Init::ExpectableWatcherImpl init_watcher_; + Stats::IsolatedStoreImpl stats_store_; + Api::ApiPtr api_; + Init::ManagerImpl init_manager_{"init_manager"}; + Stats::SinkPtr sink_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmStatSinkConfigTest, testing_values); + +TEST_P(WasmStatSinkConfigTest, CreateWasmFromEmpty) { + envoy::extensions::stat_sinks::wasm::v3::Wasm config; + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm Stat Sink "); +} + +TEST_P(WasmStatSinkConfigTest, CreateWasmFailOpen) { + envoy::extensions::stat_sinks::wasm::v3::Wasm config; + config.mutable_config()->set_fail_open(true); + EXPECT_THROW_WITH_MESSAGE(initializeWithConfig(config), Extensions::Common::Wasm::WasmException, + "Unable to create Wasm Stat Sink "); +} + +TEST_P(WasmStatSinkConfigTest, CreateWasmFromWASM) { + initializeWithConfig(config_); + + EXPECT_NE(sink_, nullptr); + NiceMock snapshot; + sink_->flush(snapshot); + NiceMock histogram; + sink_->onHistogramComplete(histogram, 0); +} + +} // namespace Wasm +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/stats_sinks/wasm/test_data/BUILD b/test/extensions/stats_sinks/wasm/test_data/BUILD new file mode 100644 index 000000000000..d3458434aec8 --- /dev/null +++ b/test/extensions/stats_sinks/wasm/test_data/BUILD @@ -0,0 +1,33 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary") + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "test_context_cpp_plugin", + srcs = [ + "test_context_cpp.cc", + "test_context_cpp_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm:well_known_names", + "//source/extensions/common/wasm/ext:envoy_null_plugin", + ], +) + +envoy_wasm_cc_binary( + name = "test_context_cpp.wasm", + srcs = ["test_context_cpp.cc"], + deps = [ + "//source/extensions/common/wasm/ext:envoy_proxy_wasm_api_lib", + ], +) diff --git a/test/extensions/stats_sinks/wasm/test_data/test_context_cpp.cc b/test/extensions/stats_sinks/wasm/test_data/test_context_cpp.cc new file mode 100644 index 000000000000..1491d1512464 --- /dev/null +++ b/test/extensions/stats_sinks/wasm/test_data/test_context_cpp.cc @@ -0,0 +1,48 @@ +// NOLINT(namespace-envoy) +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" +#include "source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +#else +#include "extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(CommonWasmTestContextCpp) + +class TestContext : public EnvoyContext { +public: + explicit TestContext(uint32_t id, RootContext* root) : EnvoyContext(id, root) {} +}; + +class TestRootContext : public EnvoyRootContext { +public: + explicit TestRootContext(uint32_t id, std::string_view root_id) : EnvoyRootContext(id, root_id) {} + + void onStatsUpdate(uint32_t result_size) override; + bool onDone() override; +}; + +static RegisterContextFactory register_TestContext(CONTEXT_FACTORY(TestContext), + ROOT_FACTORY(TestRootContext)); + +void TestRootContext::onStatsUpdate(uint32_t result_size) { + logWarn("TestRootContext::onStat"); + auto stats_buffer = getBufferBytes(WasmBufferType::CallData, 0, result_size); + auto stats = parseStatResults(stats_buffer->view()); + for (auto& e : stats.counters) { + logInfo("TestRootContext::onStat " + std::string(e.name) + ":" + std::to_string(e.delta)); + } + for (auto& e : stats.gauges) { + logInfo("TestRootContext::onStat " + std::string(e.name) + ":" + std::to_string(e.value)); + } +} + +bool TestRootContext::onDone() { + logWarn("TestRootContext::onDone " + std::to_string(id())); + return true; +} + +END_WASM_PLUGIN diff --git a/test/extensions/stats_sinks/wasm/test_data/test_context_cpp_null_plugin.cc b/test/extensions/stats_sinks/wasm/test_data/test_context_cpp_null_plugin.cc new file mode 100644 index 000000000000..88e3a18943f0 --- /dev/null +++ b/test/extensions/stats_sinks/wasm/test_data/test_context_cpp_null_plugin.cc @@ -0,0 +1,16 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace CommonWasmTestContextCpp { +NullPluginRegistry* context_registry_; +} // namespace CommonWasmTestContextCpp + +RegisterNullVmPluginFactory + register_common_wasm_test_context_cpp_plugin("CommonWasmTestContextCpp", []() { + return std::make_unique(CommonWasmTestContextCpp::context_registry_); + }); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/test/extensions/stats_sinks/wasm/wasm_stat_sink_test.cc b/test/extensions/stats_sinks/wasm/wasm_stat_sink_test.cc new file mode 100644 index 000000000000..acd4df85dbde --- /dev/null +++ b/test/extensions/stats_sinks/wasm/wasm_stat_sink_test.cc @@ -0,0 +1,129 @@ +#include "envoy/server/lifecycle_notifier.h" + +#include "extensions/common/wasm/wasm.h" + +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/wasm_base.h" + +#include "absl/types/optional.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Eq; + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +class TestContext : public ::Envoy::Extensions::Common::Wasm::Context { +public: + using ::Envoy::Extensions::Common::Wasm::Context::Context; + ~TestContext() override = default; + using ::Envoy::Extensions::Common::Wasm::Context::log; + proxy_wasm::WasmResult log(uint32_t level, absl::string_view message) override { + std::cerr << std::string(message) << "\n"; + log_(static_cast(level), message); + Extensions::Common::Wasm::Context::log(static_cast(level), message); + return proxy_wasm::WasmResult::Ok; + } + MOCK_METHOD2(log_, void(spdlog::level::level_enum level, absl::string_view message)); +}; + +class WasmCommonContextTest + : public Common::Wasm::WasmTestBase> { +public: + WasmCommonContextTest() = default; + + void setup(const std::string& code, std::string root_id = "") { + setupBase( + GetParam(), code, + [](Wasm* wasm, const std::shared_ptr& plugin) -> ContextBase* { + return new TestContext(wasm, plugin); + }, + root_id); + } + void setupContext() { + context_ = std::make_unique(wasm_->wasm().get(), root_context_->id(), plugin_); + context_->onCreate(); + } + + TestContext& rootContext() { return *static_cast(root_context_); } + TestContext& context() { return *context_; } + + std::unique_ptr context_; +}; + +// NB: this is required by VC++ which can not handle the use of macros in the macro definitions +// used by INSTANTIATE_TEST_SUITE_P. +auto testing_values = testing::Values( +#if defined(ENVOY_WASM_V8) + "v8", +#endif +#if defined(ENVOY_WASM_WAVM) + "wavm", +#endif + "null"); +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmCommonContextTest, testing_values); + +TEST_P(WasmCommonContextTest, OnStat) { + std::string code; + NiceMock snapshot_; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat( + "{{ test_rundir }}/test/extensions/stats_sinks/wasm/test_data/test_context_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestContextCpp"; + } + EXPECT_FALSE(code.empty()); + setup(code); + setupContext(); + + EXPECT_CALL(rootContext(), log_(spdlog::level::warn, Eq("TestRootContext::onStat"))); + EXPECT_CALL(rootContext(), + log_(spdlog::level::info, Eq("TestRootContext::onStat upstream_rq_2xx:1"))); + + EXPECT_CALL(rootContext(), + log_(spdlog::level::info, Eq("TestRootContext::onStat upstream_rq_5xx:2"))); + + EXPECT_CALL(rootContext(), + log_(spdlog::level::info, Eq("TestRootContext::onStat membership_total:3"))); + + EXPECT_CALL(rootContext(), + log_(spdlog::level::info, Eq("TestRootContext::onStat duration_total:4"))); + + EXPECT_CALL(rootContext(), log_(spdlog::level::warn, Eq("TestRootContext::onDone 1"))); + + NiceMock success_counter; + success_counter.name_ = "upstream_rq_2xx"; + success_counter.latch_ = 1; + success_counter.used_ = true; + + NiceMock error_5xx_counter; + error_5xx_counter.name_ = "upstream_rq_5xx"; + error_5xx_counter.latch_ = 1; + error_5xx_counter.used_ = true; + + snapshot_.counters_.push_back({1, success_counter}); + snapshot_.counters_.push_back({2, error_5xx_counter}); + + NiceMock membership_total; + membership_total.name_ = "membership_total"; + membership_total.value_ = 3; + membership_total.used_ = true; + snapshot_.gauges_.push_back(membership_total); + + NiceMock duration_total; + duration_total.name_ = "duration_total"; + duration_total.value_ = 4; + duration_total.used_ = true; + snapshot_.gauges_.push_back(duration_total); + + rootContext().onStatsUpdate(snapshot_); +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/test_common/BUILD b/test/test_common/BUILD index 4305cf7e4def..8e175627ab9c 100644 --- a/test/test_common/BUILD +++ b/test/test_common/BUILD @@ -301,6 +301,27 @@ envoy_cc_test( ], ) +envoy_cc_test_library( + name = "wasm_lib", + hdrs = ["wasm_base.h"], + deps = [ + "//source/common/stream_info:stream_info_lib", + "//source/extensions/common/wasm:wasm_interoperation_lib", + "//source/extensions/common/wasm:wasm_lib", + "//test/mocks/grpc:grpc_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/ssl:ssl_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/wasm/v3:pkg_cc_proto", + ], +) + envoy_basic_cc_library( name = "test_version_linkstamp", srcs = ["test_version_linkstamp.cc"], diff --git a/test/test_common/wasm_base.h b/test/test_common/wasm_base.h new file mode 100644 index 000000000000..d049460d3e41 --- /dev/null +++ b/test/test_common/wasm_base.h @@ -0,0 +1,150 @@ +#include + +#include "envoy/extensions/wasm/v3/wasm.pb.validate.h" +#include "envoy/server/lifecycle_notifier.h" + +#include "common/buffer/buffer_impl.h" +#include "common/http/message_impl.h" +#include "common/stats/isolated_store_impl.h" +#include "common/stream_info/stream_info_impl.h" + +#include "extensions/common/wasm/wasm.h" +#include "extensions/common/wasm/wasm_state.h" + +#include "test/mocks/grpc/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/ssl/mocks.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +#define MOCK_CONTEXT_LOG_ \ + using Context::log; \ + proxy_wasm::WasmResult log(uint32_t level, absl::string_view message) override { \ + log_(static_cast(level), message); \ + return proxy_wasm::WasmResult::Ok; \ + } \ + MOCK_METHOD2(log_, void(spdlog::level::level_enum level, absl::string_view message)) + +class DeferredRunner { +public: + ~DeferredRunner() { + if (f_) { + f_(); + } + } + void setFunction(std::function f) { f_ = f; } + +private: + std::function f_; +}; + +template class WasmTestBase : public Base { +public: + // NOLINTNEXTLINE(readability-identifier-naming) + void SetUp() override { clearCodeCacheForTesting(); } + + void setupBase(const std::string& runtime, const std::string& code, CreateContextFn create_root, + std::string root_id = "", std::string vm_configuration = "", + bool fail_open = false, std::string plugin_configuration = "") { + envoy::extensions::wasm::v3::VmConfig vm_config; + vm_config.set_vm_id("vm_id"); + vm_config.set_runtime(absl::StrCat("envoy.wasm.runtime.", runtime)); + ProtobufWkt::StringValue vm_configuration_string; + vm_configuration_string.set_value(vm_configuration); + vm_config.mutable_configuration()->PackFrom(vm_configuration_string); + vm_config.mutable_code()->mutable_local()->set_inline_bytes(code); + Api::ApiPtr api = Api::createApiForTest(stats_store_); + scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("wasm.")); + auto name = "plugin_name"; + auto vm_id = ""; + plugin_ = std::make_shared( + name, root_id, vm_id, runtime, plugin_configuration, fail_open, + envoy::config::core::v3::TrafficDirection::INBOUND, local_info_, &listener_metadata_); + // Passes ownership of root_context_. + Extensions::Common::Wasm::createWasm( + vm_config, plugin_, scope_, cluster_manager_, init_manager_, dispatcher_, *api, + lifecycle_notifier_, remote_data_provider_, + [this](WasmHandleSharedPtr wasm) { wasm_ = wasm; }, create_root); + if (wasm_) { + wasm_ = getOrCreateThreadLocalWasm( + wasm_, plugin_, dispatcher_, + [this, create_root](Wasm* wasm, const std::shared_ptr& plugin) { + root_context_ = static_cast(create_root(wasm, plugin)); + return root_context_; + }); + } + } + + WasmHandleSharedPtr& wasm() { return wasm_; } + Context* rootContext() { return root_context_; } + + DeferredRunner deferred_runner_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr scope_; + NiceMock tls_; + NiceMock dispatcher_; + NiceMock cluster_manager_; + NiceMock init_manager_; + WasmHandleSharedPtr wasm_; + PluginSharedPtr plugin_; + NiceMock ssl_; + NiceMock connection_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + NiceMock local_info_; + NiceMock lifecycle_notifier_; + envoy::config::core::v3::Metadata listener_metadata_; + Context* root_context_ = nullptr; // Unowned. + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +template class WasmHttpFilterTestBase : public WasmTestBase { +public: + template void setupFilterBase(const std::string root_id = "") { + auto wasm = WasmTestBase::wasm_ ? WasmTestBase::wasm_->wasm().get() : nullptr; + int root_context_id = wasm ? wasm->getRootContext(root_id)->id() : 0; + context_ = std::make_unique(wasm, root_context_id, WasmTestBase::plugin_); + context_->setDecoderFilterCallbacks(decoder_callbacks_); + context_->setEncoderFilterCallbacks(encoder_callbacks_); + } + + std::unique_ptr context_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + NiceMock request_stream_info_; +}; + +template +class WasmNetworkFilterTestBase : public WasmTestBase { +public: + template void setupFilterBase(const std::string root_id = "") { + auto wasm = WasmTestBase::wasm_ ? WasmTestBase::wasm_->wasm().get() : nullptr; + int root_context_id = wasm ? wasm->getRootContext(root_id)->id() : 0; + context_ = std::make_unique(wasm, root_context_id, WasmTestBase::plugin_); + context_->initializeReadFilterCallbacks(read_filter_callbacks_); + context_->initializeWriteFilterCallbacks(write_filter_callbacks_); + } + + std::unique_ptr context_; + NiceMock read_filter_callbacks_; + NiceMock write_filter_callbacks_; +}; + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/tools/wee8_compile/BUILD b/test/tools/wee8_compile/BUILD index d1184b071750..0f9fa2f4cf55 100644 --- a/test/tools/wee8_compile/BUILD +++ b/test/tools/wee8_compile/BUILD @@ -1,7 +1,7 @@ load( "//bazel:envoy_build_system.bzl", - "envoy_cc_test_binary", - "envoy_cc_test_library", + "envoy_cc_binary", + "envoy_cc_library", "envoy_package", ) @@ -9,12 +9,12 @@ licenses(["notice"]) # Apache 2 envoy_package() -envoy_cc_test_binary( +envoy_cc_binary( name = "wee8_compile_tool", deps = [":wee8_compile_lib"], ) -envoy_cc_test_library( +envoy_cc_library( name = "wee8_compile_lib", srcs = ["wee8_compile.cc"], external_deps = ["wee8"], diff --git a/test/tools/wee8_compile/wee8_compile.cc b/test/tools/wee8_compile/wee8_compile.cc index 499311b5418b..42cbfea08a18 100644 --- a/test/tools/wee8_compile/wee8_compile.cc +++ b/test/tools/wee8_compile/wee8_compile.cc @@ -1,14 +1,3 @@ -/* - * A tool to precompile Wasm modules. - * - * This is accomplished by loading and instantiating the Wasm module, serializing - * the V8 Isolate containing compiled code, and saving it in Wasm module's Custom - * Section under the "precompiled_v8_v_" name. - * - * Such precompiled Wasm module can be deserialized and loaded by V8, without the - * need to compile Wasm bytecode each time it's loaded. - */ - // NOLINT(namespace-envoy) #include @@ -21,19 +10,16 @@ #include "v8-version.h" #include "wasm-api/wasm.hh" -uint32_t InvalidVarint = ~uint32_t{0}; - -uint32_t parseVarint(const byte_t** pos, const byte_t* end) { +uint32_t parseVarint(const byte_t*& pos, const byte_t* end) { uint32_t n = 0; uint32_t shift = 0; byte_t b; do { - if (*pos >= end) { - return InvalidVarint; + if (pos + 1 > end) { + return static_cast(-1); } - b = **pos; - (*pos)++; + b = *pos++; n += (b & 0x7f) << shift; shift += 7; } while ((b & 0x80) != 0); @@ -45,11 +31,15 @@ wasm::vec getVarint(uint32_t value) { byte_t bytes[5]; int pos = 0; - while (value >= 0x80) { - bytes[pos++] = static_cast(0x80 | (value & 0x7f)); + while (pos < 5) { + if ((value & ~0x7F) == 0) { + bytes[pos++] = static_cast(value); + break; + } + + bytes[pos++] = static_cast(value & 0x7F) | 0x80; value >>= 7; } - bytes[pos++] = static_cast(value & 0x7f); auto vec = wasm::vec::make_uninitialized(pos); ::memcpy(vec.get(), bytes, pos); @@ -79,20 +69,24 @@ wasm::vec readWasmModule(const char* path, const std::string& name) { return wasm::vec::invalid(); } - // Parse Custom Sections to see if precompiled module already exists. + // Parse custom sections to see if precompiled module already exists. const byte_t* pos = content.get() + 8 /* Wasm header */; const byte_t* end = content.get() + content.size(); while (pos < end) { - const byte_t section_type = *pos++; - const uint32_t section_len = parseVarint(&pos, end); - if (section_len == InvalidVarint || section_len > static_cast(end - pos)) { + if (pos + 1 > end) { std::cerr << "ERROR: Failed to parse corrupted Wasm module from: " << path << std::endl; return wasm::vec::invalid(); } - if (section_type == 0 /* Custom Section */) { - const byte_t* section_data_start = pos; - const uint32_t section_name_len = parseVarint(&pos, end); - if (section_name_len == InvalidVarint || section_name_len > static_cast(end - pos)) { + const auto section_type = *pos++; + const auto section_len = parseVarint(pos, end); + if (section_len == static_cast(-1) || pos + section_len > end) { + std::cerr << "ERROR: Failed to parse corrupted Wasm module from: " << path << std::endl; + return wasm::vec::invalid(); + } + if (section_type == 0 /* custom section */) { + const auto section_data_start = pos; + const auto section_name_len = parseVarint(pos, end); + if (section_name_len == static_cast(-1) || pos + section_name_len > end) { std::cerr << "ERROR: Failed to parse corrupted Wasm module from: " << path << std::endl; return wasm::vec::invalid(); } @@ -121,14 +115,18 @@ wasm::vec stripWasmModule(const wasm::vec& module) { pos += 8; while (pos < end) { - const byte_t* section_start = pos; - const byte_t section_type = *pos++; - const uint32_t section_len = parseVarint(&pos, end); - if (section_len == InvalidVarint || section_len > static_cast(end - pos)) { + const auto section_start = pos; + if (pos + 1 > end) { std::cerr << "ERROR: Failed to parse corrupted Wasm module." << std::endl; return wasm::vec::invalid(); } - if (section_type != 0 /* Custom Section */) { + const auto section_type = *pos++; + const auto section_len = parseVarint(pos, end); + if (section_len == static_cast(-1) || pos + section_len > end) { + std::cerr << "ERROR: Failed to parse corrupted Wasm module." << std::endl; + return wasm::vec::invalid(); + } + if (section_type != 0 /* custom section */) { stripped.insert(stripped.end(), section_start, pos + section_len); } pos += section_len; @@ -156,8 +154,7 @@ wasm::vec serializeWasmModule(const char* path, const wasm::vec& return wasm::vec::invalid(); } - // TODO(PiotrSikora): figure out how to wait until the backgrounded (optimized) compilation is - // finished, or ideally, how to run the optimized synchronous compilation right away. + // TODO(PiotrSikora): figure out how to hook the completion callback. sleep(3); return module->serialize(); @@ -167,11 +164,11 @@ bool writeWasmModule(const char* path, const wasm::vec& module, size_t s const std::string& section_name, const wasm::vec& serialized) { auto file = std::fstream(path, std::ios::out | std::ios::binary); file.write(module.get(), module.size()); - const char section_type = '\0'; // Custom Section + const char section_type = '\0'; // custom section file.write(§ion_type, 1); - const wasm::vec section_name_len = getVarint(static_cast(section_name.size())); - const wasm::vec section_size = getVarint( - static_cast(section_name_len.size() + section_name.size() + serialized.size())); + const auto section_name_len = getVarint(section_name.size()); + const auto section_size = + getVarint(section_name_len.size() + section_name.size() + serialized.size()); file.write(section_size.get(), section_size.size()); file.write(section_name_len.get(), section_name_len.size()); file.write(section_name.data(), section_name.size()); @@ -183,48 +180,46 @@ bool writeWasmModule(const char* path, const wasm::vec& module, size_t s return false; } - const size_t total_size = module.size() + 1 + section_size.size() + section_name_len.size() + - section_name.size() + serialized.size(); + const auto total_size = module.size() + 1 + section_size.size() + section_name_len.size() + + section_name.size() + serialized.size(); std::cout << "Written " << total_size << " bytes (bytecode: " << stripped_module_size << " bytes," << " precompiled: " << serialized.size() << " bytes)." << std::endl; return true; } #if defined(__linux__) && defined(__x86_64__) -#define WEE8_WASM_PRECOMPILE_PLATFORM "linux_x86_64" -#endif - -#ifndef WEE8_WASM_PRECOMPILE_PLATFORM - -int main(int, char**) { - std::cerr << "Unsupported platform." << std::endl; - return EXIT_FAILURE; -} - +#define WEE8_PLATFORM "linux_x86_64" #else +#define WEE8_PLATFORM "" +#endif int main(int argc, char* argv[]) { + if (sizeof(WEE8_PLATFORM) - 1 == 0) { + std::cerr << "Unsupported platform." << std::endl; + return EXIT_FAILURE; + } + if (argc != 3) { std::cerr << "Usage: " << argv[0] << " " << std::endl; return EXIT_FAILURE; } - const std::string section_name = - "precompiled_v8_v" + std::to_string(V8_MAJOR_VERSION) + "." + - std::to_string(V8_MINOR_VERSION) + "." + std::to_string(V8_BUILD_NUMBER) + "." + - std::to_string(V8_PATCH_LEVEL) + "_" + WEE8_WASM_PRECOMPILE_PLATFORM; + const std::string section_name = "precompiled_wee8_v" + std::to_string(V8_MAJOR_VERSION) + "." + + std::to_string(V8_MINOR_VERSION) + "." + + std::to_string(V8_BUILD_NUMBER) + "." + + std::to_string(V8_PATCH_LEVEL) + "_" + WEE8_PLATFORM; - const wasm::vec module = readWasmModule(argv[1], section_name); + const auto module = readWasmModule(argv[1], section_name); if (!module) { return EXIT_FAILURE; } - const wasm::vec stripped_module = stripWasmModule(module); + const auto stripped_module = stripWasmModule(module); if (!stripped_module) { return EXIT_FAILURE; } - const wasm::vec serialized = serializeWasmModule(argv[1], stripped_module); + const auto serialized = serializeWasmModule(argv[1], stripped_module); if (!serialized) { return EXIT_FAILURE; } @@ -235,5 +230,3 @@ int main(int argc, char* argv[]) { return EXIT_SUCCESS; } - -#endif diff --git a/tools/check_repositories.sh b/tools/check_repositories.sh index ef3b77e194b3..0503f8153f67 100755 --- a/tools/check_repositories.sh +++ b/tools/check_repositories.sh @@ -12,8 +12,8 @@ fi # Check whether number of defined `url =` or `urls =` and `sha256 =` kwargs in # repository definitions is equal. -urls_count=$(git grep -E "\