From 983cc5ea22bcc5673d129e829d83be235fceef6f Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 22 Aug 2022 17:11:48 +0800 Subject: [PATCH 1/5] feat: add workflow plugin(mvp) --- apisix/plugins/workflow.lua | 118 ++++++++ conf/config-default.yaml | 1 + t/admin/plugins.t | 1 + t/plugin/workflow.t | 549 ++++++++++++++++++++++++++++++++++++ 4 files changed, 669 insertions(+) create mode 100644 apisix/plugins/workflow.lua create mode 100644 t/plugin/workflow.t diff --git a/apisix/plugins/workflow.lua b/apisix/plugins/workflow.lua new file mode 100644 index 000000000000..b024cc28cf31 --- /dev/null +++ b/apisix/plugins/workflow.lua @@ -0,0 +1,118 @@ +-- +-- 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 expr = require("resty.expr.v1") +local ipairs = ipairs +local tonumber = tonumber +local type = type + +local schema = { + type = "object", + properties = { + rules = { + type = "array", + items = { + type = "object", + properties = { + case = { + type = "array", + items = { + type = "array", + }, + minItems = 1, + }, + actions = { + type = "array", + items = { + type = "array", + minItems = 2 + } + } + }, + required = {"case", "actions"} + } + } + } +} + +local plugin_name = "workflow" + +local _M = { + version = 0.1, + priority = 1006, + name = plugin_name, + schema = schema +} + + +function _M.check_schema(conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + for _, rule in ipairs(conf.rules) do + local ok, err = expr.new(rule.case) + if not ok then + return false, "failed to validate the 'case' expression: " .. err + end + + local actions = rule.actions + for _, action in ipairs(actions) do + if action[1] == "return" then + if not action[2].code then + return false, "bad actions, code is needed if action is return" + end + + if type(action[2].code) ~= "number" then + return false, "bad code, the required type of code is number" + end + + return true + end + + return false, "unsupported action: " .. action[1] + end + end + + return true +end + + +local function do_action(actions) + for _, action in ipairs(actions) do + if action[1] == "return" then + local code = tonumber(action[2].code) + return core.response.exit(code) + end + end +end + + +function _M.access(conf, ctx) + local match_result + for _, rule in ipairs(conf.rules) do + local expr, _ = expr.new(rule.case) + match_result = expr:eval(ctx.var) + if match_result then + do_action(rule.actions) + end + end +end + + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 9e42add2bcdf..41262e02d8cb 100755 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -430,6 +430,7 @@ plugins: # plugin list (sorted by priority) - proxy-mirror # priority: 1010 - proxy-cache # priority: 1009 - proxy-rewrite # priority: 1008 + - workflow # priority: 1006 - api-breaker # priority: 1005 - limit-conn # priority: 1003 - limit-count # priority: 1002 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 6c89c2bb0858..9ea1ee4146c4 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -93,6 +93,7 @@ authz-keycloak proxy-mirror proxy-cache proxy-rewrite +workflow api-breaker limit-conn limit-count diff --git a/t/plugin/workflow.t b/t/plugin/workflow.t new file mode 100644 index 000000000000..6b28aeee6de1 --- /dev/null +++ b/t/plugin/workflow.t @@ -0,0 +1,549 @@ +# +# 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_long_string(); +no_root_location(); +no_shuffle(); +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests(); + + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + {"uri", "==", "/hello"} + }, + actions = { + { + "return", + { + code = 403 + } + } + } + } + } + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 2: missing actions +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + {"uri", "==", "/hello"} + } + } + } + }) + if not ok then + ngx.say(err) + return + end + + ngx.say("done") + } + } +--- response_body eval +qr/property "actions" is required/ + + + +=== TEST 3: actions have at least 2 items +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + {"uri", "==", "/hello"} + }, + actions = { + { + "return" + } + } + } + } + }) + if not ok then + ngx.say(err) + return + end + + ngx.say("done") + } + } +--- response_body eval +qr/expect array to have at least 2 items/ + + + +=== TEST 4: code is needed if action is return +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + {"uri", "==", "/hello"} + }, + actions = { + { + "return", + { + status = 403 + } + } + } + } + } + }) + if not ok then + ngx.say(err) + return + end + + ngx.say("done") + } + } +--- response_body +bad actions, code is needed if action is return + + + +=== TEST 5: the required type of code is number +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + {"uri", "==", "/hello"} + }, + actions = { + { + "return", + { + code = "403" + } + } + } + } + } + }) + if not ok then + ngx.say(err) + return + end + + ngx.say("done") + } + } +--- response_body +bad code, the required type of code is number + + + +=== TEST 6: bad conf of case +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + + }, + actions = { + { + "return", + { + code = 403 + } + } + } + } + } + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body eval +qr/property "case" validation failed: expect array to have at least 1 items/ + + + +=== TEST 7: unsupported action +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.workflow") + local ok, err = plugin.check_schema({ + rules = { + { + case = { + {"uri", "==", "/hello"} + }, + actions = { + { + "fake", + { + code = 403 + } + } + } + } + } + }) + if not ok then + ngx.say(err) + return + end + + ngx.say("done") + } + } +--- response_body +unsupported action: fake + + + +=== TEST 8: set plugin +--- 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, + [[{ + "plugins": { + "workflow": { + "rules": [ + { + "case": [ + ["uri", "==", "/hello"] + ], + "actions": [ + [ + "return", + { + "code": 403 + } + ] + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: trigger workflow +--- request +GET /hello +--- error_code: 403 + + + +=== TEST 10: multiple conditions in one case +--- 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, + [[{ + "plugins": { + "workflow": { + "rules": [ + { + "case": [ + ["uri", "==", "/hello"], + ["arg_foo", "==", "bar"] + ], + "actions": [ + [ + "return", + { + "code": 403 + } + ] + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 11: missing match the only case +--- request +GET /hello?foo=bad + + + +=== TEST 12: trigger workflow +--- request +GET /hello?foo=bar +--- error_code: 403 + + + +=== TEST 13: multiple cases with different actions +--- 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, + [[{ + "plugins": { + "workflow": { + "rules": [ + { + "case": [ + ["uri", "==", "/hello"] + ], + "actions": [ + [ + "return", + { + "code": 403 + } + ] + ] + }, + { + "case": [ + ["uri", "==", "/hello2"] + ], + "actions": [ + [ + "return", + { + "code": 401 + } + ] + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 14: trigger one case +--- request +GET /hello +--- error_code: 403 + + + +=== TEST 15: trigger another case +--- request +GET /hello2 +--- error_code: 401 + + + +=== TEST 16: match case in order +# rules is an array, match in the order of the index of the array, +# when cases are matched, actions are executed and do not continue +--- 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, + [[{ + "plugins": { + "workflow": { + "rules": [ + { + "case": [ + ["arg_foo", "==", "bar"] + ], + "actions": [ + [ + "return", + { + "code": 403 + } + ] + ] + }, + { + "case": [ + ["uri", "==", "/hello"] + ], + "actions": [ + [ + "return", + { + "code": 401 + } + ] + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 17: both case 1&2 matched, trigger the first cases +--- request +GET /hello?foo=bar +--- error_code: 403 + + + +=== TEST 18: case 1 mismatched, trigger the second cases +--- request +GET /hello?foo=bad +--- error_code: 401 + + + +=== TEST 19: all cases mismatched, pass to upstream +--- request +GET /hello1 +--- response_body +hello1 world From 1a2f6361bc461ee348fa9de3c217f72293b23c02 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 22 Aug 2022 18:10:42 +0800 Subject: [PATCH 2/5] fix code lint --- apisix/plugins/workflow.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/workflow.lua b/apisix/plugins/workflow.lua index b024cc28cf31..f4be5926c1a9 100644 --- a/apisix/plugins/workflow.lua +++ b/apisix/plugins/workflow.lua @@ -58,6 +58,10 @@ local _M = { schema = schema } +local support_action = { + ["return"] = true, +} + function _M.check_schema(conf) local ok, err = core.schema.check(schema, conf) @@ -73,6 +77,11 @@ function _M.check_schema(conf) local actions = rule.actions for _, action in ipairs(actions) do + + if not support_action[action[1]] then + return false, "unsupported action: " .. action[1] + end + if action[1] == "return" then if not action[2].code then return false, "bad actions, code is needed if action is return" @@ -81,11 +90,7 @@ function _M.check_schema(conf) if type(action[2].code) ~= "number" then return false, "bad code, the required type of code is number" end - - return true end - - return false, "unsupported action: " .. action[1] end end From ce75335b6bcef3f15e086bf21b9c559342fe77a8 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 22 Aug 2022 21:09:47 +0800 Subject: [PATCH 3/5] fix CI --- t/plugin/workflow.t | 160 +++++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/t/plugin/workflow.t b/t/plugin/workflow.t index 6b28aeee6de1..b941f38f0a4f 100644 --- a/t/plugin/workflow.t +++ b/t/plugin/workflow.t @@ -392,50 +392,52 @@ GET /hello?foo=bar --- config location /t { content_by_lua_block { + local json = require("toolkit.json") local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "workflow": { - "rules": [ + local data = { + uri = "/*", + plugins = { + workflow = { + rules = { + { + case = { + {"uri", "==", "/hello"} + }, + actions = { { - "case": [ - ["uri", "==", "/hello"] - ], - "actions": [ - [ - "return", - { - "code": 403 - } - ] - ] - }, + "return", + { + code = 403 + } + } + } + }, + { + case = { + {"uri", "==", "/hello2"} + }, + actions = { { - "case": [ - ["uri", "==", "/hello2"] - ], - "actions": [ - [ - "return", - { - "code": 401 - } - ] - ] + "return", + { + code = 401 + } } - ] + } } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/*" - }]] + } + } + }, + upstream = { + nodes = { + ["127.0.0.1:1980"] = 1 + }, + type = "roundrobin" + } + } + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + json.encode(data) ) if code >= 300 then @@ -470,50 +472,52 @@ GET /hello2 --- config location /t { content_by_lua_block { + local json = require("toolkit.json") local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "workflow": { - "rules": [ + local data = { + uri = "/*", + plugins = { + workflow = { + rules = { + { + case = { + {"arg_foo", "==", "bar"} + }, + actions = { { - "case": [ - ["arg_foo", "==", "bar"] - ], - "actions": [ - [ - "return", - { - "code": 403 - } - ] - ] - }, + "return", + { + code = 403 + } + } + } + }, + { + case = { + {"uri", "==", "/hello"} + }, + actions = { { - "case": [ - ["uri", "==", "/hello"] - ], - "actions": [ - [ - "return", - { - "code": 401 - } - ] - ] + "return", + { + code = 401 + } } - ] + } } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/*" - }]] + } + } + }, + upstream = { + nodes = { + ["127.0.0.1:1980"] = 1 + }, + type = "roundrobin" + } + } + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + json.encode(data) ) if code >= 300 then From f44ec3097154e244d7738028006f73dd331d26bd Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Tue, 23 Aug 2022 17:56:49 +0800 Subject: [PATCH 4/5] resolve code review --- apisix/plugins/workflow.lua | 45 ++++++++++++++++++++++++------------- t/plugin/workflow.t | 15 +++++++------ 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/apisix/plugins/workflow.lua b/apisix/plugins/workflow.lua index f4be5926c1a9..d49f75311777 100644 --- a/apisix/plugins/workflow.lua +++ b/apisix/plugins/workflow.lua @@ -39,7 +39,7 @@ local schema = { type = "array", items = { type = "array", - minItems = 2 + minItems = 1 } } }, @@ -58,8 +58,31 @@ local _M = { schema = schema } + +local return_schema = { + type = "object", + properties = { + code = { + type = "integer", + minimum = 100, + maximum = 599 + } + }, + required = {"code"} +} + + +local function exit(conf) + local code = tonumber(conf.code) + return code, {error_msg = "rejected by workflow"} +end + + local support_action = { - ["return"] = true, + ["return"] = { + handler = exit, + schema = return_schema, + }, } @@ -82,14 +105,9 @@ function _M.check_schema(conf) return false, "unsupported action: " .. action[1] end - if action[1] == "return" then - if not action[2].code then - return false, "bad actions, code is needed if action is return" - end - - if type(action[2].code) ~= "number" then - return false, "bad code, the required type of code is number" - end + local ok, err = core.schema.check(support_action[action[1]].schema, action[2]) + if not ok then + return false, "failed to validate the '" .. action[1] .. "' action: " .. err end end end @@ -100,10 +118,7 @@ end local function do_action(actions) for _, action in ipairs(actions) do - if action[1] == "return" then - local code = tonumber(action[2].code) - return core.response.exit(code) - end + return support_action[action[1]].handler(action[2]) end end @@ -114,7 +129,7 @@ function _M.access(conf, ctx) local expr, _ = expr.new(rule.case) match_result = expr:eval(ctx.var) if match_result then - do_action(rule.actions) + return do_action(rule.actions) end end end diff --git a/t/plugin/workflow.t b/t/plugin/workflow.t index b941f38f0a4f..dac3fdc8ba9b 100644 --- a/t/plugin/workflow.t +++ b/t/plugin/workflow.t @@ -98,7 +98,7 @@ qr/property "actions" is required/ -=== TEST 3: actions have at least 2 items +=== TEST 3: actions have at least 1 items --- config location /t { content_by_lua_block { @@ -111,7 +111,6 @@ qr/property "actions" is required/ }, actions = { { - "return" } } } @@ -126,7 +125,7 @@ qr/property "actions" is required/ } } --- response_body eval -qr/expect array to have at least 2 items/ +qr/expect array to have at least 1 items/ @@ -160,8 +159,8 @@ qr/expect array to have at least 2 items/ ngx.say("done") } } ---- response_body -bad actions, code is needed if action is return +--- response_body eval +qr/property "code" is required/ @@ -195,8 +194,8 @@ bad actions, code is needed if action is return ngx.say("done") } } ---- response_body -bad code, the required type of code is number +--- response_body eval +qr/property "code" validation failed: wrong type: expected integer, got string/ @@ -385,6 +384,8 @@ GET /hello?foo=bad --- request GET /hello?foo=bar --- error_code: 403 +--- response_body +{"error_msg":"rejected by workflow"} From 0aa25df87dcbf45889758883e423e25d2cfb66a1 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Tue, 23 Aug 2022 18:13:25 +0800 Subject: [PATCH 5/5] fix code lint --- apisix/plugins/workflow.lua | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apisix/plugins/workflow.lua b/apisix/plugins/workflow.lua index d49f75311777..a303826f6fb0 100644 --- a/apisix/plugins/workflow.lua +++ b/apisix/plugins/workflow.lua @@ -18,7 +18,6 @@ local core = require("apisix.core") local expr = require("resty.expr.v1") local ipairs = ipairs local tonumber = tonumber -local type = type local schema = { type = "object", @@ -116,20 +115,15 @@ function _M.check_schema(conf) end -local function do_action(actions) - for _, action in ipairs(actions) do - return support_action[action[1]].handler(action[2]) - end -end - - function _M.access(conf, ctx) local match_result for _, rule in ipairs(conf.rules) do local expr, _ = expr.new(rule.case) match_result = expr:eval(ctx.var) if match_result then - return do_action(rule.actions) + -- only one action is currently supported + local action = rule.actions[1] + return support_action[action[1]].handler(action[2]) end end end