diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8fc220037fe..76414f0347d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,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 d5a7d937edff..05fedad56be2 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 61e2647f94ca..9d71547c0206 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 248c868fd5cb..be4b386a8bee 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 6b78764f875f..1fee35b96a09 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -342,6 +342,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 750f5b1e0dfe..de5a623ff23d 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -298,7 +298,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"} + } + } + } + }, } } @@ -712,6 +736,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 1e060cd6e11f..efdffe47d40e 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 000000000000..d018e5973d42 --- /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 0df2fa92586f..62d11626f457 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -358,6 +358,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 904583330e5a..deabb64d9e26 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -213,6 +213,10 @@ "type": "doc", "id": "external-plugin" }, + { + "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 000000000000..5f13d7ea1bfc --- /dev/null +++ b/docs/en/latest/wasm.md @@ -0,0 +1,96 @@ +--- +title: WASM +--- + + + +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 83e28c810a7e..da9a46e75e84 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 000000000000..a8e5584a8e8c --- /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 000000000000..8dd66cb4e15f --- /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 000000000000..9a875c8144c9 --- /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 000000000000..599f22615046 --- /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 000000000000..2880b0988bb0 --- /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 000000000000..69f43d39a954 --- /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