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 cffa4b69f feat(workflow): support limit count action (#7771)
cffa4b69f is described below
commit cffa4b69f9080707d100fbe856f251054cc8cba4
Author: tzssangglass <[email protected]>
AuthorDate: Mon Aug 29 14:43:50 2022 +0800
feat(workflow): support limit count action (#7771)
---
apisix/plugins/limit-count.lua | 255 +---------
.../{limit-count.lua => limit-count/init.lua} | 65 ++-
apisix/plugins/workflow.lua | 41 +-
t/plugin/workflow.t | 533 +++++++++++++--------
t/plugin/workflow2.t | 285 +++++++++++
5 files changed, 698 insertions(+), 481 deletions(-)
diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua
index 56ff34cb2..0eafd6423 100644
--- a/apisix/plugins/limit-count.lua
+++ b/apisix/plugins/limit-count.lua
@@ -14,271 +14,24 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-local limit_local_new = require("resty.limit.count").new
-local core = require("apisix.core")
-local apisix_plugin = require("apisix.plugin")
-local tab_insert = table.insert
-local ipairs = ipairs
-local pairs = pairs
-
+local limit_count = require("apisix.plugins.limit-count.init")
local plugin_name = "limit-count"
-local limit_redis_cluster_new
-local limit_redis_new
-do
- local redis_src = "apisix.plugins.limit-count.limit-count-redis"
- limit_redis_new = require(redis_src).new
-
- local cluster_src = "apisix.plugins.limit-count.limit-count-redis-cluster"
- limit_redis_cluster_new = require(cluster_src).new
-end
-local lrucache = core.lrucache.new({
- type = 'plugin', serial_creating = true,
-})
-local group_conf_lru = core.lrucache.new({
- type = 'plugin',
-})
-
-
-local policy_to_additional_properties = {
- redis = {
- properties = {
- redis_host = {
- type = "string", minLength = 2
- },
- redis_port = {
- type = "integer", minimum = 1, default = 6379,
- },
- redis_password = {
- type = "string", minLength = 0,
- },
- redis_database = {
- type = "integer", minimum = 0, default = 0,
- },
- redis_timeout = {
- type = "integer", minimum = 1, default = 1000,
- },
- },
- required = {"redis_host"},
- },
- ["redis-cluster"] = {
- properties = {
- redis_cluster_nodes = {
- type = "array",
- minItems = 2,
- items = {
- type = "string", minLength = 2, maxLength = 100
- },
- },
- redis_password = {
- type = "string", minLength = 0,
- },
- redis_timeout = {
- type = "integer", minimum = 1, default = 1000,
- },
- redis_cluster_name = {
- type = "string",
- },
- },
- required = {"redis_cluster_nodes", "redis_cluster_name"},
- },
-}
-local schema = {
- type = "object",
- properties = {
- count = {type = "integer", exclusiveMinimum = 0},
- time_window = {type = "integer", exclusiveMinimum = 0},
- group = {type = "string"},
- key = {type = "string", default = "remote_addr"},
- key_type = {type = "string",
- enum = {"var", "var_combination", "constant"},
- default = "var",
- },
- rejected_code = {
- type = "integer", minimum = 200, maximum = 599, default = 503
- },
- rejected_msg = {
- type = "string", minLength = 1
- },
- policy = {
- type = "string",
- enum = {"local", "redis", "redis-cluster"},
- default = "local",
- },
- allow_degradation = {type = "boolean", default = false},
- show_limit_quota_header = {type = "boolean", default = true}
- },
- required = {"count", "time_window"},
- ["if"] = {
- properties = {
- policy = {
- enum = {"redis"},
- },
- },
- },
- ["then"] = policy_to_additional_properties.redis,
- ["else"] = {
- ["if"] = {
- properties = {
- policy = {
- enum = {"redis-cluster"},
- },
- },
- },
- ["then"] = policy_to_additional_properties["redis-cluster"],
- }
-}
-
-local schema_copy = core.table.deepcopy(schema)
-
local _M = {
version = 0.4,
priority = 1002,
name = plugin_name,
- schema = schema,
+ schema = limit_count.schema,
}
-local function group_conf(conf)
- return conf
-end
-
-
function _M.check_schema(conf)
- local ok, err = core.schema.check(schema, conf)
- if not ok then
- return false, err
- end
-
- if conf.group then
- local fields = {}
- -- When the goup field is configured,
- -- we will use schema_copy to get the whitelist of properties,
- -- so that we can avoid getting injected properties.
- for k in pairs(schema_copy.properties) do
- tab_insert(fields, k)
- end
- local extra = policy_to_additional_properties[conf.policy]
- if extra then
- for k in pairs(extra.properties) do
- tab_insert(fields, k)
- end
- end
-
- local prev_conf = group_conf_lru(conf.group, "", group_conf, conf)
-
- for _, field in ipairs(fields) do
- if not core.table.deep_eq(prev_conf[field], conf[field]) then
- core.log.error("previous limit-conn group ", prev_conf.group,
- " conf: ", core.json.encode(prev_conf))
- core.log.error("current limit-conn group ", conf.group,
- " conf: ", core.json.encode(conf))
- return false, "group conf mismatched"
- end
- end
- end
-
- return true
-end
-
-
-local function create_limit_obj(conf)
- core.log.info("create new limit-count plugin instance")
-
- if not conf.policy or conf.policy == "local" then
- return limit_local_new("plugin-" .. plugin_name, conf.count,
- conf.time_window)
- end
-
- if conf.policy == "redis" then
- return limit_redis_new("plugin-" .. plugin_name,
- conf.count, conf.time_window, conf)
- end
-
- if conf.policy == "redis-cluster" then
- return limit_redis_cluster_new("plugin-" .. plugin_name, conf.count,
- conf.time_window, conf)
- end
-
- return nil
+ return limit_count.check_schema(conf)
end
function _M.access(conf, ctx)
- core.log.info("ver: ", ctx.conf_version)
-
- local lim, err
- if not conf.group then
- lim, err = core.lrucache.plugin_ctx(lrucache, ctx, conf.policy,
create_limit_obj, conf)
- else
- lim, err = lrucache(conf.group, "", create_limit_obj, conf)
- end
-
- if not lim then
- core.log.error("failed to fetch limit.count object: ", err)
- if conf.allow_degradation then
- return
- end
- return 500
- end
-
- local conf_key = conf.key
- local key
- if conf.key_type == "var_combination" then
- local err, n_resolved
- key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var)
- if err then
- core.log.error("could not resolve vars in ", conf_key, " error: ",
err)
- end
-
- if n_resolved == 0 then
- key = nil
- end
- elseif conf.key_type == "constant" then
- key = conf_key
- else
- key = ctx.var[conf_key]
- end
-
- if key == nil then
- core.log.info("The value of the configured key is empty, use client IP
instead")
- -- When the value of key is empty, use client IP instead
- key = ctx.var["remote_addr"]
- end
-
- -- here we add a separator ':' to mark the boundary of the prefix and the
key itself
- if not conf.group then
- -- Here we use plugin-level conf version to prevent the counter from
being resetting
- -- because of the change elsewhere.
- -- A route which reuses a previous route's ID will inherits its
counter.
- key = ctx.conf_type .. ctx.conf_id .. ':' ..
apisix_plugin.conf_version(conf) .. ':' .. key
- else
- key = conf.group .. ':' .. key
- end
-
- core.log.info("limit key: ", key)
-
- local delay, remaining = lim:incoming(key, true)
- if not delay then
- local err = remaining
- if err == "rejected" then
- if conf.rejected_msg then
- return conf.rejected_code, { error_msg = conf.rejected_msg }
- end
- return conf.rejected_code
- end
-
- core.log.error("failed to limit count: ", err)
- if conf.allow_degradation then
- return
- end
- return 500, {error_msg = "failed to limit count"}
- end
-
- if conf.show_limit_quota_header then
- core.response.set_header("X-RateLimit-Limit", conf.count,
- "X-RateLimit-Remaining", remaining)
- end
+ return limit_count.rate_limit(conf, ctx)
end
diff --git a/apisix/plugins/limit-count.lua
b/apisix/plugins/limit-count/init.lua
similarity index 85%
copy from apisix/plugins/limit-count.lua
copy to apisix/plugins/limit-count/init.lua
index 56ff34cb2..c9051d2e1 100644
--- a/apisix/plugins/limit-count.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -132,10 +132,7 @@ local schema = {
local schema_copy = core.table.deepcopy(schema)
local _M = {
- version = 0.4,
- priority = 1002,
- name = plugin_name,
- schema = schema,
+ schema = schema
}
@@ -151,6 +148,11 @@ function _M.check_schema(conf)
end
if conf.group then
+ -- means that call by some plugin not support
+ if conf._vid then
+ return false, "group is not supported"
+ end
+
local fields = {}
-- When the goup field is configured,
-- we will use schema_copy to get the whitelist of properties,
@@ -204,16 +206,48 @@ local function create_limit_obj(conf)
end
-function _M.access(conf, ctx)
- core.log.info("ver: ", ctx.conf_version)
+local function gen_limit_key(conf, ctx, key)
+ if conf.group then
+ return conf.group .. ':' .. key
+ end
+
+ -- here we add a separator ':' to mark the boundary of the prefix and the
key itself
+ -- Here we use plugin-level conf version to prevent the counter from being
resetting
+ -- because of the change elsewhere.
+ -- A route which reuses a previous route's ID will inherits its counter.
+ local new_key = ctx.conf_type .. ctx.conf_id .. ':' ..
apisix_plugin.conf_version(conf)
+ .. ':' .. key
+ if conf._vid then
+ -- conf has _vid means it's from workflow plugin, add _vid to the key
+ -- so that the counter is unique per action.
+ return new_key .. ':' .. conf._vid
+ end
+
+ return new_key
+end
+
+
+local function gen_limit_obj(conf, ctx)
+ if conf.group then
+ return lrucache(conf.group, "", create_limit_obj, conf)
+ end
- local lim, err
- if not conf.group then
- lim, err = core.lrucache.plugin_ctx(lrucache, ctx, conf.policy,
create_limit_obj, conf)
+ local extra_key
+ if conf._vid then
+ extra_key = conf.policy .. '#' .. conf._vid
else
- lim, err = lrucache(conf.group, "", create_limit_obj, conf)
+ extra_key = conf.policy
end
+ return core.lrucache.plugin_ctx(lrucache, ctx, extra_key,
create_limit_obj, conf)
+end
+
+
+function _M.rate_limit(conf, ctx)
+ core.log.info("ver: ", ctx.conf_version)
+
+ local lim, err = gen_limit_obj(conf, ctx)
+
if not lim then
core.log.error("failed to fetch limit.count object: ", err)
if conf.allow_degradation then
@@ -246,16 +280,7 @@ function _M.access(conf, ctx)
key = ctx.var["remote_addr"]
end
- -- here we add a separator ':' to mark the boundary of the prefix and the
key itself
- if not conf.group then
- -- Here we use plugin-level conf version to prevent the counter from
being resetting
- -- because of the change elsewhere.
- -- A route which reuses a previous route's ID will inherits its
counter.
- key = ctx.conf_type .. ctx.conf_id .. ':' ..
apisix_plugin.conf_version(conf) .. ':' .. key
- else
- key = conf.group .. ':' .. key
- end
-
+ key = gen_limit_key(conf, ctx, key)
core.log.info("limit key: ", key)
local delay, remaining = lim:incoming(key, true)
diff --git a/apisix/plugins/workflow.lua b/apisix/plugins/workflow.lua
index a303826f6..a586a923b 100644
--- a/apisix/plugins/workflow.lua
+++ b/apisix/plugins/workflow.lua
@@ -14,10 +14,10 @@
-- 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 core = require("apisix.core")
+local limit_count = require("apisix.plugins.limit-count.init")
+local expr = require("resty.expr.v1")
+local ipairs = ipairs
local schema = {
type = "object",
@@ -71,17 +71,34 @@ local return_schema = {
}
+local function check_return_schema(conf)
+ local ok, err = core.schema.check(return_schema, conf)
+ if not ok then
+ return false, err
+ end
+ return true
+end
+
+
local function exit(conf)
- local code = tonumber(conf.code)
- return code, {error_msg = "rejected by workflow"}
+ return conf.code, {error_msg = "rejected by workflow"}
+end
+
+
+local function rate_limit(conf, ctx)
+ return limit_count.rate_limit(conf, ctx)
end
local support_action = {
["return"] = {
- handler = exit,
- schema = return_schema,
+ handler = exit,
+ check_schema = check_return_schema,
},
+ ["limit-count"] = {
+ handler = rate_limit,
+ check_schema = limit_count.check_schema,
+ }
}
@@ -91,7 +108,7 @@ function _M.check_schema(conf)
return false, err
end
- for _, rule in ipairs(conf.rules) do
+ for idx, 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
@@ -104,7 +121,9 @@ function _M.check_schema(conf)
return false, "unsupported action: " .. action[1]
end
- local ok, err =
core.schema.check(support_action[action[1]].schema, action[2])
+ -- use the action's idx as an identifier to isolate between confs
+ action[2]["_vid"] = idx
+ local ok, err = support_action[action[1]].check_schema(action[2],
plugin_name)
if not ok then
return false, "failed to validate the '" .. action[1] .. "'
action: " .. err
end
@@ -123,7 +142,7 @@ function _M.access(conf, ctx)
if match_result then
-- only one action is currently supported
local action = rule.actions[1]
- return support_action[action[1]].handler(action[2])
+ return support_action[action[1]].handler(action[2], ctx)
end
end
end
diff --git a/t/plugin/workflow.t b/t/plugin/workflow.t
index dac3fdc8b..e1bf77a1f 100644
--- a/t/plugin/workflow.t
+++ b/t/plugin/workflow.t
@@ -37,238 +37,143 @@ run_tests();
__DATA__
-=== TEST 1: sanity
+=== TEST 1: schema check
--- 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",
+ local data = {
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
{
- code = 403
+ "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"}
+ },
+ {
+ 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 1 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 = {
- {
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ }
}
}
}
- }
- })
- if not ok then
- ngx.say(err)
- return
- end
-
- ngx.say("done")
- }
- }
---- response_body eval
-qr/expect array to have at least 1 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",
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
{
- status = 403
+ "return",
+ {
+ status = 403
+ }
}
}
}
}
- }
- })
- if not ok then
- ngx.say(err)
- return
- end
-
- ngx.say("done")
- }
- }
---- response_body eval
-qr/property "code" is required/
-
-
-
-=== 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",
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
{
- code = "403"
+ "return",
+ {
+ code = "403"
+ }
}
}
}
}
- }
- })
- if not ok then
- ngx.say(err)
- return
- end
-
- ngx.say("done")
- }
- }
---- response_body eval
-qr/property "code" validation failed: wrong type: expected integer, got string/
-
-
-
-=== 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 = {
+ },
+ {
+ rules = {
+ {
+ case = {
- },
- actions = {
- {
- "return",
+ },
+ actions = {
{
- code = 403
+ "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",
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
{
- code = 403
+ "fake",
+ {
+ code = 403
+ }
}
}
}
}
}
- })
- if not ok then
- ngx.say(err)
- return
- end
+ }
- ngx.say("done")
+ for _, conf in ipairs(data) do
+ local ok, err = plugin.check_schema(conf)
+ if not ok then
+ ngx.say(err)
+ else
+ ngx.say("done")
+ end
+ end
}
}
--- response_body
+done
+property "rules" validation failed: failed to validate item 1: property
"actions" is required
+property "rules" validation failed: failed to validate item 1: property
"actions" validation failed: failed to validate item 1: expect array to have at
least 1 items
+failed to validate the 'return' action: property "code" is required
+failed to validate the 'return' action: property "code" validation failed:
wrong type: expected integer, got string
+property "rules" validation failed: failed to validate item 1: property "case"
validation failed: expect array to have at least 1 items
unsupported action: fake
-=== TEST 8: set plugin
+=== TEST 2: set plugin
--- config
location /t {
content_by_lua_block {
@@ -317,14 +222,14 @@ passed
-=== TEST 9: trigger workflow
+=== TEST 3: trigger workflow
--- request
GET /hello
--- error_code: 403
-=== TEST 10: multiple conditions in one case
+=== TEST 4: multiple conditions in one case
--- config
location /t {
content_by_lua_block {
@@ -374,13 +279,13 @@ passed
-=== TEST 11: missing match the only case
+=== TEST 5: missing match the only case
--- request
GET /hello?foo=bad
-=== TEST 12: trigger workflow
+=== TEST 6: trigger workflow
--- request
GET /hello?foo=bar
--- error_code: 403
@@ -389,7 +294,7 @@ GET /hello?foo=bar
-=== TEST 13: multiple cases with different actions
+=== TEST 7: multiple cases with different actions
--- config
location /t {
content_by_lua_block {
@@ -453,21 +358,21 @@ passed
-=== TEST 14: trigger one case
+=== TEST 8: trigger one case
--- request
GET /hello
--- error_code: 403
-=== TEST 15: trigger another case
+=== TEST 9: trigger another case
--- request
GET /hello2
--- error_code: 401
-=== TEST 16: match case in order
+=== TEST 10: 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
@@ -533,22 +438,252 @@ passed
-=== TEST 17: both case 1&2 matched, trigger the first cases
+=== TEST 11: 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
+=== TEST 12: case 1 mismatched, trigger the second cases
--- request
GET /hello?foo=bad
--- error_code: 401
-=== TEST 19: all cases mismatched, pass to upstream
+=== TEST 13: all cases mismatched, pass to upstream
--- request
GET /hello1
--- response_body
hello1 world
+
+
+
+=== TEST 14: schema check(limit-count)
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.workflow")
+ local data = {
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {count = 2, time_window = 60,
rejected_code = 503, key = 'remote_addr'}
+ }
+ }
+ }
+ }
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {count = 2}
+ }
+ }
+ }
+ }
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {time_window = 60}
+ }
+ }
+ }
+ }
+ },
+ {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 2,
+ time_window = 60,
+ rejected_code = 503,
+ group = "services_1"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for _, conf in ipairs(data) do
+ local ok, err = plugin.check_schema(conf)
+ if not ok then
+ ngx.say(err)
+ else
+ ngx.say("done")
+ end
+ end
+ }
+ }
+--- response_body
+done
+failed to validate the 'limit-count' action: property "time_window" is required
+failed to validate the 'limit-count' action: property "count" is required
+failed to validate the 'limit-count' action: group is not supported
+
+
+
+=== TEST 15: set actions as limit-count
+--- 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": [
+ [
+ "limit-count",
+ {
+ "count": 3,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ },
+ "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 16: up the limit
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 200, 503]
+
+
+
+=== TEST 17: the conf in actions is isolation
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin").test
+ local data = {
+ uri = "/*",
+ plugins = {
+ workflow = {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 3,
+ time_window = 60,
+ rejected_code = 503,
+ key = "remote_addr"
+ }
+ }
+ }
+ },
+ {
+ case = {
+ {"uri", "==", "/hello1"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 3,
+ time_window = 60,
+ rejected_code = 503,
+ key = "remote_addr"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ 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
+ ngx.status = code
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 18: cross-hit case 1 and case 2, up limit by isolation
+--- pipelined_requests eval
+["GET /hello", "GET /hello1", "GET /hello", "GET /hello1",
+"GET /hello", "GET /hello1", "GET /hello", "GET /hello1"]
+--- error_code eval
+[200, 200, 200, 200, 200, 200, 503, 503]
diff --git a/t/plugin/workflow2.t b/t/plugin/workflow2.t
new file mode 100644
index 000000000..b30567532
--- /dev/null
+++ b/t/plugin/workflow2.t
@@ -0,0 +1,285 @@
+#
+# 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.
+#
+
+BEGIN {
+ if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+ $SkipReason = "unavailable for the hup tests";
+
+ } else {
+ $ENV{TEST_NGINX_USE_HUP} = 1;
+ undef $ENV{TEST_NGINX_USE_STAP};
+ }
+}
+
+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: multiple cases with different actions(return & limit-count)
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin").test
+ local data = {
+ uri = "/*",
+ plugins = {
+ workflow = {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "return",
+ {
+ code = 403
+ }
+ }
+ }
+ },
+ {
+ case = {
+ {"uri", "==", "/hello1"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 1,
+ time_window = 60,
+ rejected_code = 503
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ 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
+ ngx.status = code
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 2: cross-hit case 1 and case 2, trigger actions by isolation
+--- pipelined_requests eval
+["GET /hello", "GET /hello1", "GET /hello1"]
+--- error_code eval
+[403, 200, 503]
+
+
+
+=== TEST 3: the conf in actions is isolation
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin").test
+ local data = {
+ uri = "/*",
+ plugins = {
+ workflow = {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 3,
+ time_window = 60,
+ rejected_code = 503,
+ key = "remote_addr"
+ }
+ }
+ }
+ },
+ {
+ case = {
+ {"uri", "==", "/hello1"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 3,
+ time_window = 60,
+ rejected_code = 503,
+ key = "remote_addr"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ 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
+ ngx.status = code
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: cross-hit case 1 and case 2, trigger actions by isolation
+--- pipelined_requests eval
+["GET /hello", "GET /hello1", "GET /hello", "GET /hello1"]
+--- error_code eval
+[200, 200, 200, 200]
+
+
+
+=== TEST 5: cross-hit case 1 and case 2, up limit by isolation 2
+--- pipelined_requests eval
+["GET /hello", "GET /hello1", "GET /hello", "GET /hello1"]
+--- error_code eval
+[200, 200, 503, 503]
+
+
+
+=== TEST 6: different actions with different limit count conf, up limit by
isolation
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin").test
+ local data = {
+ uri = "/*",
+ plugins = {
+ workflow = {
+ rules = {
+ {
+ case = {
+ {"uri", "==", "/hello"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 1,
+ time_window = 60,
+ rejected_code = 503,
+ key = "remote_addr"
+ }
+ }
+ }
+ },
+ {
+ case = {
+ {"uri", "==", "/hello1"}
+ },
+ actions = {
+ {
+ "limit-count",
+ {
+ count = 2,
+ time_window = 60,
+ rejected_code = 503,
+ key = "remote_addr"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ 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
+ ngx.status = code
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 7: case 1 up limit, case 2 psssed
+--- pipelined_requests eval
+["GET /hello", "GET /hello1", "GET /hello", "GET /hello1"]
+--- error_code eval
+[200, 200, 503, 200]