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

Reply via email to