This is an automated email from the ASF dual-hosted git repository.
spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new fa8a34f feat: initial wasm support (#5288)
fa8a34f is described below
commit fa8a34f72d4de45a42390d17ca27aa9f808deb83
Author: 罗泽轩 <[email protected]>
AuthorDate: Fri Oct 22 16:29:12 2021 +0800
feat: initial wasm support (#5288)
---
.github/workflows/build.yml | 8 ++
.gitignore | 1 +
.licenserc.yaml | 4 +-
README.md | 1 +
apisix/cli/ngx_tpl.lua | 4 +
apisix/cli/ops.lua | 27 +++-
apisix/plugin.lua | 65 +++++++--
apisix/wasm.lua | 120 ++++++++++++++++
conf/config-default.yaml | 6 +
docs/en/latest/config.json | 4 +
docs/en/latest/wasm.md | 96 +++++++++++++
t/APISIX.pm | 1 +
t/cli/test_wasm.sh | 66 +++++++++
t/wasm/global-rule.t | 186 ++++++++++++++++++++++++
t/wasm/go.mod | 6 +
t/wasm/go.sum | 8 ++
t/wasm/log/main.go | 81 +++++++++++
t/wasm/route.t | 342 ++++++++++++++++++++++++++++++++++++++++++++
18 files changed, 1008 insertions(+), 18 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3597b40..93597b6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -72,6 +72,14 @@ jobs:
- name: Linux Get dependencies
run: sudo apt install -y cpanminus build-essential libncurses5-dev
libreadline-dev libssl-dev perl libpcre3 libpcre3-dev libldap2-dev
+ - name: Build wasm code
+ if: startsWith(matrix.os_name, 'linux_openresty')
+ run: |
+ export TINYGO_VER=0.20.0
+ wget
https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VER}/tinygo_${TINYGO_VER}_amd64.deb
2>/dev/null
+ sudo dpkg -i tinygo_${TINYGO_VER}_amd64.deb
+ cd t/wasm && find . -type f -name "main.go" | xargs -Ip tinygo build
-o p.wasm -scheduler=none -target=wasi p
+
- name: Linux Before install
run: sudo ./ci/${{ matrix.os_name }}_runner.sh before_install
diff --git a/.gitignore b/.gitignore
index d5a7d93..05fedad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ build-cache/
t/fuzzing/__pycache__/
boofuzz-results/
*.pyc
+*.wasm
# release tar package
*.tgz
release/*
diff --git a/.licenserc.yaml b/.licenserc.yaml
index 61e2647..9d71547 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -35,8 +35,8 @@ header:
- '**/*.log'
# Exclude test toolkit files
- 't/toolkit'
- - 't/chaos/go.mod'
- - 't/chaos/go.sum'
+ - 'go.mod'
+ - 'go.sum'
# Exclude non-Apache licensed files
- 'apisix/balancer/ewma.lua'
# Exclude plugin-specific configuration files
diff --git a/README.md b/README.md
index 248c868..be4b386 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,7 @@ A/B testing, canary release, blue-green deployment, limit
rate, defense against
- **Highly scalable**
- [Custom plugins](docs/en/latest/plugin-develop.md): Allows hooking of
common phases, such as `rewrite`, `access`, `header filter`, `body filter` and
`log`, also allows to hook the `balancer` stage.
- [Plugin can be written in
Java/Go/Python](docs/en/latest/external-plugin.md)
+ - [Plugin can be written with Proxy WASM SDK](docs/en/latest/wasm.md)
- Custom load balancing algorithms: You can use custom load balancing
algorithms during the `balancer` phase.
- Custom routing: Support users to implement routing algorithms themselves.
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index 80760b7..f5fa5d6 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -348,6 +348,10 @@ http {
}
{% end %}
+ {% if wasm then %}
+ wasm_vm wasmtime;
+ {% end %}
+
init_by_lua_block {
require "resty.core"
{% if lua_module_hook then %}
diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua
index 6856868..ed9abe9 100644
--- a/apisix/cli/ops.lua
+++ b/apisix/cli/ops.lua
@@ -338,7 +338,31 @@ local config_schema = {
}
}
}
- }
+ },
+ wasm = {
+ type = "object",
+ properties = {
+ plugins = {
+ type = "array",
+ minItems = 1,
+ items = {
+ type = "object",
+ properties = {
+ name = {
+ type = "string"
+ },
+ file = {
+ type = "string"
+ },
+ priority = {
+ type = "integer"
+ }
+ },
+ required = {"name", "file", "priority"}
+ }
+ }
+ }
+ },
}
}
@@ -752,6 +776,7 @@ Please modify "admin_key" in conf/config.yaml .
for k,v in pairs(yaml_conf.nginx_config) do
sys_conf[k] = v
end
+ sys_conf["wasm"] = yaml_conf.wasm
local wrn = sys_conf["worker_rlimit_nofile"]
diff --git a/apisix/plugin.lua b/apisix/plugin.lua
index 1e060cd..efdffe4 100644
--- a/apisix/plugin.lua
+++ b/apisix/plugin.lua
@@ -18,6 +18,7 @@ local require = require
local core = require("apisix.core")
local config_util = require("apisix.core.config_util")
local enable_debug = require("apisix.debug").enable_debug
+local wasm = require("apisix.wasm")
local ngx_exit = ngx.exit
local pkg_loaded = package.loaded
local sort_tab = table.sort
@@ -67,9 +68,16 @@ local function sort_plugin(l, r)
end
-local function unload_plugin(name, is_stream_plugin)
+local PLUGIN_TYPE_HTTP = 1
+local PLUGIN_TYPE_STREAM = 2
+local PLUGIN_TYPE_HTTP_WASM = 3
+local function unload_plugin(name, plugin_type)
+ if plugin_type == PLUGIN_TYPE_HTTP_WASM then
+ return
+ end
+
local pkg_name = "apisix.plugins." .. name
- if is_stream_plugin then
+ if plugin_type == PLUGIN_TYPE_STREAM then
pkg_name = "apisix.stream.plugins." .. name
end
@@ -82,13 +90,21 @@ local function unload_plugin(name, is_stream_plugin)
end
-local function load_plugin(name, plugins_list, is_stream_plugin)
- local pkg_name = "apisix.plugins." .. name
- if is_stream_plugin then
- pkg_name = "apisix.stream.plugins." .. name
+local function load_plugin(name, plugins_list, plugin_type)
+ local ok, plugin
+ if plugin_type == PLUGIN_TYPE_HTTP_WASM then
+ -- for wasm plugin, we pass the whole attrs instead of name
+ ok, plugin = wasm.require(name)
+ name = name.name
+ else
+ local pkg_name = "apisix.plugins." .. name
+ if plugin_type == PLUGIN_TYPE_STREAM then
+ pkg_name = "apisix.stream.plugins." .. name
+ end
+
+ ok, plugin = pcall(require, pkg_name)
end
- local ok, plugin = pcall(require, pkg_name)
if not ok then
core.log.error("failed to load plugin [", name, "] err: ", plugin)
return
@@ -140,25 +156,39 @@ local function load_plugin(name, plugins_list,
is_stream_plugin)
end
-local function load(plugin_names)
+local function load(plugin_names, wasm_plugin_names)
local processed = {}
for _, name in ipairs(plugin_names) do
if processed[name] == nil then
processed[name] = true
end
end
+ for _, attrs in ipairs(wasm_plugin_names) do
+ if processed[attrs.name] == nil then
+ processed[attrs.name] = attrs
+ end
+ end
core.log.warn("new plugins: ", core.json.delay_encode(processed))
- for name in pairs(local_plugins_hash) do
- unload_plugin(name)
+ for name, plugin in pairs(local_plugins_hash) do
+ local ty = PLUGIN_TYPE_HTTP
+ if plugin.type == "wasm" then
+ ty = PLUGIN_TYPE_HTTP_WASM
+ end
+ unload_plugin(name, ty)
end
core.table.clear(local_plugins)
core.table.clear(local_plugins_hash)
- for name in pairs(processed) do
- load_plugin(name, local_plugins)
+ for name, value in pairs(processed) do
+ local ty = PLUGIN_TYPE_HTTP
+ if type(value) == "table" then
+ ty = PLUGIN_TYPE_HTTP_WASM
+ name = value
+ end
+ load_plugin(name, local_plugins, ty)
end
-- sort by plugin's priority
@@ -192,14 +222,14 @@ local function load_stream(plugin_names)
core.log.warn("new plugins: ", core.json.delay_encode(processed))
for name in pairs(stream_local_plugins_hash) do
- unload_plugin(name, true)
+ unload_plugin(name, PLUGIN_TYPE_STREAM)
end
core.table.clear(stream_local_plugins)
core.table.clear(stream_local_plugins_hash)
for name in pairs(processed) do
- load_plugin(name, stream_local_plugins, true)
+ load_plugin(name, stream_local_plugins, PLUGIN_TYPE_STREAM)
end
-- sort by plugin's priority
@@ -260,7 +290,12 @@ function _M.load(config)
if not http_plugin_names then
core.log.error("failed to read plugin list from local file")
else
- local ok, err = load(http_plugin_names)
+ local wasm_plugin_names = {}
+ if local_conf.wasm then
+ wasm_plugin_names = local_conf.wasm.plugins
+ end
+
+ local ok, err = load(http_plugin_names, wasm_plugin_names)
if not ok then
core.log.error("failed to load plugins: ", err)
end
diff --git a/apisix/wasm.lua b/apisix/wasm.lua
new file mode 100644
index 0000000..d018e59
--- /dev/null
+++ b/apisix/wasm.lua
@@ -0,0 +1,120 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core = require("apisix.core")
+local support_wasm, wasm = pcall(require, "resty.proxy-wasm")
+local concat = table.concat
+
+
+local schema = {
+ type = "object",
+ properties = {
+ conf = {
+ type = "string"
+ },
+ },
+ required = {"conf"}
+}
+local _M = {}
+
+
+local function check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local get_plugin_ctx_key
+do
+ local key_buf = {
+ nil,
+ nil,
+ }
+
+ function get_plugin_ctx_key(ctx)
+ key_buf[1] = ctx.conf_type
+ key_buf[2] = ctx.conf_id
+ return concat(key_buf, "#", 1, 2)
+ end
+end
+
+local function fetch_plugin_ctx(conf, ctx, plugin)
+ if not conf.plugin_ctxs then
+ conf.plugin_ctxs = {}
+ end
+
+ local ctxs = conf.plugin_ctxs
+ local key = get_plugin_ctx_key(ctx)
+ local plugin_ctx = ctxs[key]
+ local err
+ if not plugin_ctx then
+ plugin_ctx, err = wasm.on_configure(plugin, conf.conf)
+ if not plugin_ctx then
+ return nil, err
+ end
+
+ ctxs[key] = plugin_ctx
+ end
+
+ return plugin_ctx
+end
+
+
+local function access_wrapper(self, conf, ctx)
+ local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)
+ if not plugin_ctx then
+ core.log.error("failed to init wasm plugin ctx: ", err)
+ return 503
+ end
+
+ local ok, err = wasm.on_http_request_headers(plugin_ctx)
+ if not ok then
+ core.log.error("failed to run wasm plugin: ", err)
+ return 503
+ end
+end
+
+
+function _M.require(attrs)
+ if not support_wasm then
+ return nil, "need to build APISIX-OpenResty to support wasm"
+ end
+
+ local name = attrs.name
+ local priority = attrs.priority
+ local plugin, err = wasm.load(name, attrs.file)
+ if not plugin then
+ return nil, err
+ end
+
+ local mod = {
+ version = 0.1,
+ name = name,
+ priority = priority,
+ schema = schema,
+ check_schema = check_schema,
+ plugin = plugin,
+ type = "wasm",
+ }
+ mod.access = function (conf, ctx)
+ return access_wrapper(mod, conf, ctx)
+ end
+
+ -- the returned values need to be the same as the Lua's 'require'
+ return true, mod
+end
+
+
+return _M
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index 32ed56b..54e760d 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -361,6 +361,12 @@ stream_plugins: # sorted by priority
- mqtt-proxy # priority: 1000
# <- recommend to use priority (0, 100) for your custom plugins
+#wasm:
+ #plugins:
+ #- name: wasm_log
+ #priority: 7999
+ #file: t/wasm/log/main.go.wasm
+
plugin_attr:
log-rotate:
interval: 3600 # rotate interval (unit: second)
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 9045833..deabb64 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -215,6 +215,10 @@
},
{
"type": "doc",
+ "id": "wasm"
+ },
+ {
+ "type": "doc",
"id": "plugin-interceptors"
},
{
diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md
new file mode 100644
index 0000000..5f13d7e
--- /dev/null
+++ b/docs/en/latest/wasm.md
@@ -0,0 +1,96 @@
+---
+title: WASM
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+APISIX supports WASM plugins written with [Proxy WASM
SDK](https://github.com/proxy-wasm/spec#sdks).
+
+This plugin requires APISIX to run on
[APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix),
and is under construction.
+Currently, only a few APIs are implemented. Please follow
[wasm-nginx-module](https://github.com/api7/wasm-nginx-module) to know the
progress.
+
+## Programming model
+
+The plugin supports the follwing concepts from Proxy WASM:
+
+```
+ Wasm Virtual Machine
+┌────────────────────────────────────────────────────────────────┐
+│ Your Plugin │
+│ │ │
+│ │ 1: 1 │
+│ │ 1: N │
+│ VMContext ────────── PluginContext │
+│ ╲ 1: N │
+│ ╲ │
+│ ╲ HttpContext │
+│ (Http stream) │
+└────────────────────────────────────────────────────────────────┘
+```
+
+* All plugins run in the same WASM VM, like the Lua plugin in the Lua VM
+* Each plugin has its own VMContext (the root ctx)
+* Each configured route/global rules has its own PluginContext (the plugin
ctx).
+For example, if we have a service configuring with WASM plugin, and two routes
inherit from it,
+there will be two plugin ctxs.
+* Each HTTP request which hits the configuration will have its own HttpContext
(the HTTP ctx).
+For example, if we configure both global rules and route, the HTTP request will
+have two HTTP ctxs, one for the plugin ctx from global rules and the other for
the
+plugin ctx from route.
+
+## How to use
+
+First of all, we need to define the plugin in `config.yaml`:
+
+```yaml
+wasm:
+ plugins:
+ - name: wasm_log # the name of the plugin
+ priority: 7999 # priority
+ file: t/wasm/log/main.go.wasm # the path of `.wasm` file
+```
+
+That's all. Now you can use the wasm plugin as a regular plugin.
+
+For example, enable this plugin on the specified route:
+
+```shell
+curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY:
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "uri": "/index.html",
+ "plugins": {
+ "wasm_log": {
+ "conf": "blahblah"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ }
+}'
+```
+
+Attributes below can be configured in the plugin:
+
+| Name | Type | Requirement | Default | Valid
|
Description
|
+| --------------------------------------| ------------| -------------- |
-------- | --------------------------------------------------------------- |
---------------------------------------------------------------------------------------------------------------------------------------------------
|
+| conf | string | required | | | the plugin ctx
configuration which can be fetched via Proxy WASM SDK |
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 83e28c8..da9a46e 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -212,6 +212,7 @@ my $a6_ngx_directives = "";
if ($version =~ m/\/apisix-nginx-module/) {
$a6_ngx_directives = <<_EOC_;
apisix_delay_client_max_body_check on;
+ wasm_vm wasmtime;
_EOC_
}
diff --git a/t/cli/test_wasm.sh b/t/cli/test_wasm.sh
new file mode 100755
index 0000000..a8e5584
--- /dev/null
+++ b/t/cli/test_wasm.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+. ./t/cli/common.sh
+
+exit_if_not_customed_nginx
+
+echo '
+wasm:
+ plugins:
+ - name: wasm_log
+ file: t/wasm/log/main.go.wasm
+' > conf/config.yaml
+
+out=$(make init 2>&1 || true)
+if ! echo "$out" | grep 'property "priority" is required'; then
+ echo "failed: priority is required"
+ exit 1
+fi
+
+echo '
+wasm:
+ plugins:
+ - name: wasm_log
+ priority: 888
+' > conf/config.yaml
+
+out=$(make init 2>&1 || true)
+if ! echo "$out" | grep 'property "file" is required'; then
+ echo "failed: file is required"
+ exit 1
+fi
+
+echo "passed: wasm configuration is validated"
+
+echo '
+wasm:
+ plugins:
+ - name: wasm_log
+ priority: 7999
+ file: t/wasm/log/main.go.wasm
+ ' > conf/config.yaml
+
+make init
+if ! grep "wasm_vm " conf/nginx.conf; then
+ echo "failed: wasm isn't enabled"
+ exit 1
+fi
+
+echo "passed: wasm is enabled"
diff --git a/t/wasm/global-rule.t b/t/wasm/global-rule.t
new file mode 100644
index 0000000..8dd66cb
--- /dev/null
+++ b/t/wasm/global-rule.t
@@ -0,0 +1,186 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX;
+
+my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
+my $version = eval { `$nginx_binary -V 2>&1` };
+
+if ($version !~ m/\/apisix-nginx-module/) {
+ plan(skip_all => "apisix-nginx-module not installed");
+} else {
+ plan('no_plan');
+}
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+ $block->set_value("no_error_log", "[error]");
+ }
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ my $extra_yaml_config = <<_EOC_;
+wasm:
+ plugins:
+ - name: wasm_log
+ priority: 7999
+ file: t/wasm/log/main.go.wasm
+ - name: wasm_log2
+ priority: 7998
+ file: t/wasm/log/main.go.wasm
+_EOC_
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/hello",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "wasm_log": {
+ "conf": "blahblah"
+ },
+ "wasm_log2": {
+ "conf": "zzz"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 2: hit
+--- request
+GET /hello
+--- grep_error_log eval
+qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/
+--- grep_error_log_out
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 1 with conf zzz in http ctx 2
+
+
+
+=== TEST 3: global rule + route
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "wasm_log2": {
+ "conf": "www"
+ }
+ },
+ "uri": "/hello"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: hit
+--- request
+GET /hello
+--- grep_error_log eval
+qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/
+--- grep_error_log_out
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 1 with conf zzz in http ctx 2
+run plugin ctx 3 with conf www in http ctx 4
+
+
+
+=== TEST 5: delete global rule
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
ngx.HTTP_DELETE)
+ if code >= 300 then
+ ngx.status = code
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
diff --git a/t/wasm/go.mod b/t/wasm/go.mod
new file mode 100644
index 0000000..9a875c8
--- /dev/null
+++ b/t/wasm/go.mod
@@ -0,0 +1,6 @@
+module github.com/api7/wasm-nginx-module
+
+go 1.15
+
+require github.com/tetratelabs/proxy-wasm-go-sdk
v0.14.1-0.20210819090022-1e4e69881a31
+//replace github.com/tetratelabs/proxy-wasm-go-sdk => ../proxy-wasm-go-sdk
diff --git a/t/wasm/go.sum b/t/wasm/go.sum
new file mode 100644
index 0000000..599f226
--- /dev/null
+++ b/t/wasm/go.sum
@@ -0,0 +1,8 @@
+github.com/davecgh/go-spew v1.1.0/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0/go.mod
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31
h1:V3GXN5nayOdIU3NypbxVegGFCVGm78qOA8Q7wkeudy8=
+github.com/tetratelabs/proxy-wasm-go-sdk
v0.14.1-0.20210819090022-1e4e69881a31/go.mod
h1:qZ+4i6e2wHlhnhgpH0VG4QFzqd2BEvQbQFU0npt2e2k=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod
h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/t/wasm/log/main.go b/t/wasm/log/main.go
new file mode 100644
index 0000000..2880b09
--- /dev/null
+++ b/t/wasm/log/main.go
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+ "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
+ "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
+)
+
+func main() {
+ proxywasm.SetVMContext(&vmContext{})
+}
+
+type vmContext struct {
+ // Embed the default VM context here,
+ // so that we don't need to reimplement all the methods.
+ types.DefaultVMContext
+}
+
+func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
+ return &pluginContext{contextID: contextID}
+}
+
+type pluginContext struct {
+ // Embed the default plugin context here,
+ // so that we don't need to reimplement all the methods.
+ types.DefaultPluginContext
+ conf string
+ contextID uint32
+}
+
+func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int)
types.OnPluginStartStatus {
+ data, err := proxywasm.GetPluginConfiguration()
+ if err != nil {
+ proxywasm.LogCriticalf("error reading plugin configuration:
%v", err)
+ return types.OnPluginStartStatusFailed
+ }
+
+ ctx.conf = string(data)
+ return types.OnPluginStartStatusOK
+}
+
+func (ctx *pluginContext) OnPluginDone() bool {
+ proxywasm.LogInfo("do clean up...")
+ return true
+}
+
+func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
+ return &httpLifecycle{pluginCtxID: ctx.contextID, conf: ctx.conf,
contextID: contextID}
+}
+
+type httpLifecycle struct {
+ // Embed the default http context here,
+ // so that we don't need to reimplement all the methods.
+ types.DefaultHttpContext
+ pluginCtxID uint32
+ contextID uint32
+ conf string
+}
+
+func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream
bool) types.Action {
+ proxywasm.LogWarnf("run plugin ctx %d with conf %s in http ctx %d",
+ ctx.pluginCtxID, ctx.conf, ctx.contextID)
+ // TODO: support access/modify http request headers
+ return types.ActionContinue
+}
diff --git a/t/wasm/route.t b/t/wasm/route.t
new file mode 100644
index 0000000..69f43d3
--- /dev/null
+++ b/t/wasm/route.t
@@ -0,0 +1,342 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX;
+
+my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
+my $version = eval { `$nginx_binary -V 2>&1` };
+
+if ($version !~ m/\/apisix-nginx-module/) {
+ plan(skip_all => "apisix-nginx-module not installed");
+} else {
+ plan('no_plan');
+}
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+ $block->set_value("no_error_log", "[error]");
+ }
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ my $extra_yaml_config = <<_EOC_;
+wasm:
+ plugins:
+ - name: wasm_log
+ priority: 7999
+ file: t/wasm/log/main.go.wasm
+ - name: wasm_log2
+ priority: 7998
+ file: t/wasm/log/main.go.wasm
+_EOC_
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: check schema
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin").test
+ for _, case in ipairs({
+ {input = {
+ }},
+ {input = {
+ conf = {}
+ }},
+ }) do
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ {
+ id = "1",
+ uri = "/echo",
+ upstream = {
+ type = "roundrobin",
+ nodes = {}
+ },
+ plugins = {
+ wasm_log = case.input
+ }
+ }
+ )
+ ngx.say(json.decode(body).error_msg)
+ end
+ }
+ }
+--- response_body
+failed to check the configuration of plugin wasm_log err: property "conf" is
required
+failed to check the configuration of plugin wasm_log err: property "conf"
validation failed: wrong type: expected string, got table
+
+
+
+=== TEST 2: sanity
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/hello",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "wasm_log": {
+ "conf": "blahblah"
+ },
+ "wasm_log2": {
+ "conf": "zzz"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 3: hit
+--- request
+GET /hello
+--- grep_error_log eval
+qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/
+--- grep_error_log_out
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 1 with conf zzz in http ctx 2
+
+
+
+=== TEST 4: plugin from service
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/services/1',
+ ngx.HTTP_PUT,
+ [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "wasm_log": {
+ "id": "log",
+ "conf": "blahblah"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/hello",
+ "service_id": "1",
+ "hosts": ["foo.com"]
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/hello",
+ "service_id": "1",
+ "hosts": ["bar.com"]
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 5: hit
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+
+ for i = 1, 4 do
+ local host = "foo.com"
+ if i % 2 == 0 then
+ host = "bar.com"
+ end
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, {headers = {host =
host}})
+ if not res then
+ ngx.say(err)
+ return
+ end
+ end
+ }
+ }
+--- grep_error_log eval
+qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/
+--- grep_error_log_out
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 3 with conf blahblah in http ctx 4
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 3 with conf blahblah in http ctx 4
+
+
+
+=== TEST 6: plugin from plugin_config
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/plugin_configs/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "wasm_log": {
+ "id": "log",
+ "conf": "blahblah"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "uri": "/hello",
+ "plugin_config_id": "1",
+ "hosts": ["foo.com"]
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_PUT,
+ [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "uri": "/hello",
+ "plugin_config_id": "1",
+ "hosts": ["bar.com"]
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 7: hit
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+
+ for i = 1, 4 do
+ local host = "foo.com"
+ if i % 2 == 0 then
+ host = "bar.com"
+ end
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, {headers = {host =
host}})
+ if not res then
+ ngx.say(err)
+ return
+ end
+ end
+ }
+ }
+--- grep_error_log eval
+qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/
+--- grep_error_log_out
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 3 with conf blahblah in http ctx 4
+run plugin ctx 1 with conf blahblah in http ctx 2
+run plugin ctx 3 with conf blahblah in http ctx 4