You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/08/05 06:11:31 UTC

[apisix] branch master updated: feat: script distribute and run (#1982)

This is an automated email from the ASF dual-hosted git repository.

membphis 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 65df727  feat: script distribute and run (#1982)
65df727 is described below

commit 65df727af4edf91339c79f00d7559ebb2f22158c
Author: nic-chen <33...@users.noreply.github.com>
AuthorDate: Wed Aug 5 14:11:22 2020 +0800

    feat: script distribute and run (#1982)
    
    Supporting distribution and execution scripts, we can implement many new features,
    such as plug-in orchestration.
---
 apisix/admin/routes.lua          |  12 +++
 apisix/admin/services.lua        |  13 ++++
 apisix/init.lua                  |  35 ++++++---
 apisix/schema_def.lua            |  11 +++
 apisix/script.lua                |  59 +++++++++++++++
 doc/admin-api.md                 |   1 +
 doc/architecture-design.md       |  20 +++++
 doc/zh-cn/admin-api.md           |  11 +--
 doc/zh-cn/architecture-design.md |  22 ++++++
 t/admin/routes.t                 |  39 ++++++++++
 t/script/script_distribute.t     | 158 +++++++++++++++++++++++++++++++++++++++
 t/script/script_test.lua         |  43 +++++++++++
 12 files changed, 407 insertions(+), 17 deletions(-)

diff --git a/apisix/admin/routes.lua b/apisix/admin/routes.lua
index d917435..eac54c4 100644
--- a/apisix/admin/routes.lua
+++ b/apisix/admin/routes.lua
@@ -124,6 +124,18 @@ local function check_conf(id, conf, need_id)
         end
     end
 
+    if conf.script then
+        local obj, err = loadstring(conf.script)
+        if not obj then
+            return nil, {error_msg = "failed to load 'script' string: "
+                                     .. err}
+        end
+
+        if type(obj()) ~= "table" then
+            return nil, {error_msg = "'script' should be a Lua object"}
+        end
+    end
+
     return need_id and id or true
 end
 
diff --git a/apisix/admin/services.lua b/apisix/admin/services.lua
index cbce7d1..2200333 100644
--- a/apisix/admin/services.lua
+++ b/apisix/admin/services.lua
@@ -21,6 +21,7 @@ local upstreams = require("apisix.admin.upstreams")
 local tostring = tostring
 local ipairs = ipairs
 local type = type
+local loadstring = loadstring
 
 
 local _M = {
@@ -91,6 +92,18 @@ local function check_conf(id, conf, need_id)
         end
     end
 
+    if conf.script then
+        local obj, err = loadstring(conf.script)
+        if not obj then
+            return nil, {error_msg = "failed to load 'script' string: "
+                                     .. err}
+        end
+
+        if type(obj()) ~= "table" then
+            return nil, {error_msg = "'script' should be a Lua object"}
+        end
+    end
+
     return need_id and id or true
 end
 
diff --git a/apisix/init.lua b/apisix/init.lua
index 29536de..bbb22a7 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -18,6 +18,7 @@ local require       = require
 local core          = require("apisix.core")
 local config_util   = require("apisix.core.config_util")
 local plugin        = require("apisix.plugin")
+local script        = require("apisix.script")
 local service_fetch = require("apisix.http.service").get
 local admin_init    = require("apisix.admin.init")
 local get_var       = require("resty.ngxvar").fetch
@@ -452,19 +453,24 @@ function _M.http_access_phase()
         api_ctx.var.upstream_connection = api_ctx.var.http_connection
     end
 
-    local plugins = plugin.filter(route)
-    api_ctx.plugins = plugins
-
-    run_plugin("rewrite", plugins, api_ctx)
-    if api_ctx.consumer then
-        local changed
-        route, changed = plugin.merge_consumer_route(route, api_ctx.consumer)
-        if changed then
-            core.table.clear(api_ctx.plugins)
-            api_ctx.plugins = plugin.filter(route, api_ctx.plugins)
+    if route.value.script then
+        script.load(route, api_ctx)
+        script.run("access", api_ctx)
+    else
+        local plugins = plugin.filter(route)
+        api_ctx.plugins = plugins
+
+        run_plugin("rewrite", plugins, api_ctx)
+        if api_ctx.consumer then
+            local changed
+            route, changed = plugin.merge_consumer_route(route, api_ctx.consumer)
+            if changed then
+                core.table.clear(api_ctx.plugins)
+                api_ctx.plugins = plugin.filter(route, api_ctx.plugins)
+            end
         end
+        run_plugin("access", plugins, api_ctx)
     end
-    run_plugin("access", plugins, api_ctx)
 
     local ok, err = set_upstream(route, api_ctx)
     if not ok then
@@ -553,7 +559,12 @@ local function common_phase(phase_name)
         core.tablepool.release("plugins", plugins)
     end
 
-    run_plugin(phase_name, nil, api_ctx)
+    if api_ctx.script_obj then
+        script.run(phase_name, api_ctx)
+    else
+        run_plugin(phase_name, nil, api_ctx)
+    end
+
     return api_ctx
 end
 
diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua
index 1b537d3..9e4604f 100644
--- a/apisix/schema_def.lua
+++ b/apisix/schema_def.lua
@@ -422,6 +422,8 @@ _M.route = {
             pattern = [[^function]],
         },
 
+        script = {type = "string", minLength = 10, maxLength = 102400},
+
         plugins = plugins_schema,
         upstream = upstream_schema,
 
@@ -441,6 +443,13 @@ _M.route = {
         {required = {"upstream", "uris"}},
         {required = {"upstream_id", "uris"}},
         {required = {"service_id", "uris"}},
+        {required = {"script", "uri"}},
+        {required = {"script", "uris"}},
+    },
+    ["not"] = {
+        anyOf = {
+            {required = {"script", "plugins"}}
+        }
     },
     additionalProperties = false,
 }
@@ -455,11 +464,13 @@ _M.service = {
         upstream_id = id_schema,
         name = {type = "string", maxLength = 50},
         desc = {type = "string", maxLength = 256},
+        script = {type = "string", minLength = 10, maxLength = 102400},
     },
     anyOf = {
         {required = {"upstream"}},
         {required = {"upstream_id"}},
         {required = {"plugins"}},
+        {required = {"script"}},
     },
     additionalProperties = false,
 }
diff --git a/apisix/script.lua b/apisix/script.lua
new file mode 100644
index 0000000..a1e0c02
--- /dev/null
+++ b/apisix/script.lua
@@ -0,0 +1,59 @@
+--
+-- 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 require    = require
+local core       = require("apisix.core")
+local loadstring = loadstring
+local error      = error
+
+
+local _M = {}
+
+
+function _M.load(route, api_ctx)
+    local script = route.value.script
+    if script == nil or script == "" then
+        error("missing valid script")
+    end
+
+    local loadfun, err = loadstring(script, "route#" .. route.value.id)
+    if not loadfun then
+        error("failed to load script: " .. err .. " script: " .. script)
+        return nil
+    end
+    api_ctx.script_obj = loadfun()
+end
+
+
+function _M.run(phase, api_ctx)
+    local obj = api_ctx and api_ctx.script_obj
+    if not obj then
+        core.log.error("missing loaded script object")
+        return api_ctx
+    end
+
+    core.log.info("loaded script_obj: ", core.json.delay_encode(obj, true))
+
+    local phase_fun = obj[phase]
+    if phase_fun then
+        phase_fun(api_ctx)
+    end
+
+    return api_ctx
+end
+
+
+return _M
diff --git a/doc/admin-api.md b/doc/admin-api.md
index 665f585..9a3def9 100644
--- a/doc/admin-api.md
+++ b/doc/admin-api.md
@@ -64,6 +64,7 @@
 |vars       |False  |Match Rules |A list of one or more `{var, operator, val}` elements, like this: `{{var, operator, val}, {var, operator, val}, ...}}`. For example: `{"arg_name", "==", "json"}` means that the current request parameter `name` is `json`. The `var` here is consistent with the internal variable name of Nginx, so you can also use `request_uri`, `host`, etc. For the operator part, the currently supported operators are `==`, `~=`,`>`, `<`, and `~~`. For the `>` and `<` operat [...]
 |filter_func|False|Match Rules|User-defined filtering function. You can use it to achieve matching requirements for special scenarios. This function accepts an input parameter named `vars` by default, which you can use to get Nginx variables.|function(vars) return vars["arg_name"] == "json" end|
 |plugins  |False |Plugin|See [Plugin](architecture-design.md#plugin) for more ||
+|script  |False |Script|See [Script](architecture-design.md#script) for more ||
 |upstream |False |Upstream|Enabled Upstream configuration, see [Upstream](architecture-design.md#upstream) for more||
 |upstream_id|False |Upstream|Enabled upstream id, see [Upstream](architecture-design.md#upstream) for more ||
 |service_id|False |Service|Binded Service configuration, see [Service](architecture-design.md#service) for more ||
diff --git a/doc/architecture-design.md b/doc/architecture-design.md
index ece3ff5..d52ba30 100644
--- a/doc/architecture-design.md
+++ b/doc/architecture-design.md
@@ -26,6 +26,7 @@
 - [**Route**](#route)
 - [**Service**](#service)
 - [**Plugin**](#plugin)
+- [**Script**](#script)
 - [**Upstream**](#upstream)
 - [**Router**](#router)
 - [**Consumer**](#consumer)
@@ -216,6 +217,25 @@ Not all plugins have specific configuration items. For example, there is no spec
 
 [Back to top](#Table-of-contents)
 
+## Script
+
+`Script` represents a script that will be executed during the `HTTP` request/response life cycle.
+
+The `Script` configuration can be directly bound to the `Route`.
+
+`Script` and `Plugin` are mutually exclusive, and `Script` is executed first. This means that after configuring `Script`, the `Plugin` configured on `Route` will not be executed.
+
+In theory, you can write arbitrary Lua code in `Script`, or you can directly call existing plugins to reuse existing code.
+
+`Script` also has the concept of execution phase, supporting `access`, `header_filer`, `body_filter` and `log` phase. The system will automatically execute the code of the corresponding phase in the `Script` script in the corresponding phase.
+
+```json
+{
+    ...
+    "script": "local _M = {} \n function _M.access(api_ctx) \n ngx.log(ngx.INFO,\"hit access phase\") \n end \nreturn _M"
+}
+```
+
 ## Upstream
 
 Upstream is a virtual host abstraction that performs load balancing on a given set of service nodes according to configuration rules. Upstream address information can be directly configured to `Route` (or `Service`). When Upstream has duplicates, you need to use "reference" to avoid duplication.
diff --git a/doc/zh-cn/admin-api.md b/doc/zh-cn/admin-api.md
index 1638420..529d102 100644
--- a/doc/zh-cn/admin-api.md
+++ b/doc/zh-cn/admin-api.md
@@ -57,10 +57,11 @@
 |---------|---------|----|-----------|----|
 |uri      |与 `uris` 二选一 |匹配规则|除了如 `/foo/bar`、`/foo/gloo` 这种全量匹配外,使用不同 [Router](architecture-design.md#router) 还允许更高级匹配,更多见 [Router](architecture-design.md#router)。|"/hello"|
 |uris     |与 `uri` 二选一 |匹配规则|数组形式,可以匹配多个 `uri`|["/hello", "/world"]|
-|plugins  |`plugins`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Plugin|详见 [Plugin](architecture-design.md#plugin) ||
-|upstream |`plugins`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Upstream|启用的 Upstream 配置,详见 [Upstream](architecture-design.md#upstream)||
-|upstream_id|`plugins`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Upstream|启用的 upstream id,详见 [Upstream](architecture-design.md#upstream)||
-|service_id|`plugins`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Service|绑定的 Service 配置,详见 [Service](architecture-design.md#service)||
+|plugins  |`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Plugin|详见 [Plugin](architecture-design.md#plugin) ||
+|script  |`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Script|详见 [Script](architecture-design.md#script) ||
+|upstream |`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Upstream|启用的 Upstream 配置,详见 [Upstream](architecture-design.md#upstream)||
+|upstream_id|`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Upstream|启用的 upstream id,详见 [Upstream](architecture-design.md#upstream)||
+|service_id|`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Service|绑定的 Service 配置,详见 [Service](architecture-design.md#service)||
 |service_protocol|可选|上游协议类型|只可以是 "grpc", "http" 二选一。|默认 "http",使用gRPC proxy 或gRPC transcode 时,必须用"grpc"|
 |name     |可选 |辅助   |标识路由名称|route-xxxx|
 |desc     |可选 |辅助   |标识描述、使用场景等。|客户 xxxx|
@@ -75,7 +76,7 @@
 
 有两点需要特别注意:
 
-* 除了 `uri`/`uris` 是必选的之外,`plugins`、`upstream`/`upstream_id`、`service_id` 这三类必须选择其中至少一个。
+* 除了 `uri`/`uris` 是必选的之外,`plugins`、`script`、`upstream`/`upstream_id`、`service_id` 这三类必须选择其中至少一个。
 * 对于同一类参数比如 `uri`与 `uris`,`upstream` 与 `upstream_id`,`host` 与 `hosts`,`remote_addr` 与 `remote_addrs` 等,是不能同时存在,二者只能选择其一。如果同时启用,接口会报错。
 
 route 对象 json 配置内容:
diff --git a/doc/zh-cn/architecture-design.md b/doc/zh-cn/architecture-design.md
index ecacc58..2d696cc 100644
--- a/doc/zh-cn/architecture-design.md
+++ b/doc/zh-cn/architecture-design.md
@@ -24,6 +24,7 @@
 - [**Route**](#route)
 - [**Service**](#service)
 - [**Plugin**](#plugin)
+- [**Script**](#script)
 - [**Upstream**](#upstream)
 - [**Router**](#router)
 - [**Consumer**](#consumer)
@@ -220,6 +221,27 @@ curl http://127.0.0.1:9080/apisix/admin/routes/102 -H 'X-API-KEY: edd1c9f034335f
 
 [返回目录](#目录)
 
+## Script
+
+`Script` 表示将在 `HTTP` 请求/响应生命周期期间执行的脚本。
+
+`Script` 配置可直接绑定在 `Route` 上。
+
+`Script` 与 `Plugin` 互斥,且优先执行 `Script` ,这意味着配置 `Script` 后,`Route` 上配置的 `Plugin` 将不被执行。
+
+理论上,在 `Script` 中可以写任意 lua 代码,也可以直接调用已有插件以重用已有的代码。
+
+`Script` 也有执行阶段概念,支持 `access`、`header_filer`、`body_filter` 和 `log` 阶段。系统会在相应阶段中自动执行 `Script` 脚本中对应阶段的代码。
+
+```json
+{
+    ...
+    "script": "local _M = {} \n function _M.access(api_ctx) \n ngx.log(ngx.INFO,\"hit access phase\") \n end \nreturn _M"
+}
+```
+
+[返回目录](#目录)
+
 ## Upstream
 
 Upstream 是虚拟主机抽象,对给定的多个服务节点按照配置规则进行负载均衡。Upstream 的地址信息可以直接配置到 `Route`(或 `Service`) 上,当 Upstream 有重复时,就需要用“引用”方式避免重复了。
diff --git a/t/admin/routes.t b/t/admin/routes.t
index a5e4b6e..afa846a 100644
--- a/t/admin/routes.t
+++ b/t/admin/routes.t
@@ -2108,3 +2108,42 @@ GET /t
 {"error_msg":"invalid request body: request size 1678025 is greater than the maximum size 1572864 allowed"}
 --- error_log
 failed to read request body: request size 1678025 is greater than the maximum size 1572864 allowed
+
+
+
+=== TEST 58: uri + plugins + script  failed
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, message, res = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "limit-count": {
+                                "count": 2,
+                                "time_window": 60,
+                                "rejected_code": 503,
+                                "key": "remote_addr"
+                            }
+                        },
+                        "script": "local _M = {} \n function _M.access(api_ctx) \n ngx.log(ngx.INFO,\"hit access phase\") \n end \nreturn _M",
+                        "uri": "/index.html"
+                }]]
+                )
+
+            if code ~= 200 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body_like
+{"error_msg":"invalid configuration: value wasn't supposed to match schema"}
+--- no_error_log
+[error]
diff --git a/t/script/script_distribute.t b/t/script/script_distribute.t
new file mode 100644
index 0000000..75e7b13
--- /dev/null
+++ b/t/script/script_distribute.t
@@ -0,0 +1,158 @@
+#
+# 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 'no_plan';
+
+repeat_each(1);
+no_root_location();
+no_shuffle();
+
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set route(host + uri)
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin")
+
+            local script = t.read_file("t/script/script_test.lua")
+            local data = {
+                script = script,
+                uri = "/hello",
+                upstream = {
+                    nodes = {
+                        ["127.0.0.1:1980"] = 1
+                    },
+                    type = "roundrobin"
+                }
+            }
+
+            local code, body = t.test('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                core.json.encode(data))
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- yaml_config eval: $::yaml_config
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: hit routes
+--- request
+GET /hello
+--- yaml_config eval: $::yaml_config
+--- response_body
+hello world
+--- no_error_log
+[error]
+--- error_log
+string "route#1"
+phase_fun(): hit access phase
+phase_fun(): hit header_filter phase
+phase_fun(): hit body_filter phase
+phase_fun(): hit body_filter phase
+phase_fun(): hit log phase while
+
+
+
+=== TEST 3: invalid script in route
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin")
+
+            local data = {
+                script = "invalid script",
+                uri = "/hello",
+                upstream = {
+                    nodes = {
+                        ["127.0.0.1:1980"] = 1
+                    },
+                    type = "roundrobin"
+                }
+            }
+
+            local code, body = t.test('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                core.json.encode(data))
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- yaml_config eval: $::yaml_config
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to load 'script' string: [string \"invalid script\"]:1: '=' expected near 'script'"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: invalid script in service
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin")
+
+            local data = {
+                script = "invalid script",
+                upstream = {
+                    nodes = {
+                        ["127.0.0.1:1980"] = 1
+                    },
+                    type = "roundrobin"
+                }
+            }
+
+            local code, body = t.test('/apisix/admin/services/1',
+                ngx.HTTP_PUT,
+                core.json.encode(data))
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- yaml_config eval: $::yaml_config
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to load 'script' string: [string \"invalid script\"]:1: '=' expected near 'script'"}
+--- no_error_log
+[error]
diff --git a/t/script/script_test.lua b/t/script/script_test.lua
new file mode 100644
index 0000000..e2590ab
--- /dev/null
+++ b/t/script/script_test.lua
@@ -0,0 +1,43 @@
+--
+-- 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 _M = {}
+
+
+function _M.access(api_ctx)
+    core.log.warn("hit access phase")
+end
+
+
+function _M.header_filter(ctx)
+    core.log.warn("hit header_filter phase")
+end
+
+
+function _M.body_filter(ctx)
+    core.log.warn("hit body_filter phase")
+end
+
+
+function _M.log(ctx)
+    core.log.warn("hit log phase")
+end
+
+
+return _M