diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua index b6c3422f1987..74858c7ab446 100644 --- a/apisix/admin/init.lua +++ b/apisix/admin/init.lua @@ -53,6 +53,7 @@ local resources = { global_rules = require("apisix.admin.global_rules"), stream_routes = require("apisix.admin.stream_routes"), plugin_metadata = require("apisix.admin.plugin_metadata"), + plugin_configs = require("apisix.admin.plugin_config"), } diff --git a/apisix/admin/plugin_config.lua b/apisix/admin/plugin_config.lua new file mode 100644 index 000000000000..27c4767d12ac --- /dev/null +++ b/apisix/admin/plugin_config.lua @@ -0,0 +1,173 @@ +-- +-- 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 utils = require("apisix.admin.utils") +local schema_plugin = require("apisix.admin.plugins").check_schema +local type = type +local tostring = tostring + + +local _M = { +} + + +local function check_conf(id, conf, need_id) + if not conf then + return nil, {error_msg = "missing configurations"} + end + + id = id or conf.id + if need_id and not id then + return nil, {error_msg = "missing id"} + end + + if not need_id and id then + return nil, {error_msg = "wrong id, do not need it"} + end + + if need_id and conf.id and tostring(conf.id) ~= tostring(id) then + return nil, {error_msg = "wrong id"} + end + + conf.id = id + + core.log.info("conf: ", core.json.delay_encode(conf)) + local ok, err = core.schema.check(core.schema.global_rule, conf) + if not ok then + return nil, {error_msg = "invalid configuration: " .. err} + end + + local ok, err = schema_plugin(conf.plugins) + if not ok then + return nil, {error_msg = err} + end + + return true +end + + +function _M.put(id, conf) + local ok, err = check_conf(id, conf, true) + if not ok then + return 400, err + end + + local key = "/plugin_configs/" .. id + + local ok, err = utils.inject_conf_with_prev_conf("route", key, conf) + if not ok then + return 500, {error_msg = err} + end + + local res, err = core.etcd.set(key, conf) + if not res then + core.log.error("failed to put global rule[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +function _M.get(id) + local key = "/plugin_configs" + if id then + key = key .. "/" .. id + end + local res, err = core.etcd.get(key, not id) + if not res then + core.log.error("failed to get global rule[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +function _M.delete(id) + local key = "/plugin_configs/" .. id + local res, err = core.etcd.delete(key) + if not res then + core.log.error("failed to delete global rule[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +function _M.patch(id, conf, sub_path) + if not id then + return 400, {error_msg = "missing global rule id"} + end + + if not conf then + return 400, {error_msg = "missing new configuration"} + end + + if not sub_path or sub_path == "" then + if type(conf) ~= "table" then + return 400, {error_msg = "invalid configuration"} + end + end + + local key = "/plugin_configs/" .. id + local res_old, err = core.etcd.get(key) + if not res_old then + core.log.error("failed to get plugin config [", key, "]: ", err) + return 500, {error_msg = err} + end + + if res_old.status ~= 200 then + return res_old.status, res_old.body + end + core.log.info("key: ", key, " old value: ", + core.json.delay_encode(res_old, true)) + + local node_value = res_old.body.node.value + local modified_index = res_old.body.node.modifiedIndex + + if sub_path and sub_path ~= "" then + local code, err, node_val = core.table.patch(node_value, sub_path, conf) + node_value = node_val + if code then + return code, err + end + else + node_value = core.table.merge(node_value, conf); + end + + core.log.info("new conf: ", core.json.delay_encode(node_value, true)) + + utils.inject_timestamp(node_value, nil, conf) + + local ok, err = check_conf(id, node_value, true) + if not ok then + return 400, err + end + + local res, err = core.etcd.atomic_set(key, node_value, nil, modified_index) + if not res then + core.log.error("failed to set new plugin config[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +return _M diff --git a/apisix/admin/routes.lua b/apisix/admin/routes.lua index e5b2cb15113e..28d02729d38e 100644 --- a/apisix/admin/routes.lua +++ b/apisix/admin/routes.lua @@ -108,6 +108,23 @@ local function check_conf(id, conf, need_id) end end + local plugin_config_id = conf.plugin_config_id + if plugin_config_id then + local key = "/plugin_configs/" .. plugin_config_id + local res, err = core.etcd.get(key) + if not res then + return nil, {error_msg = "failed to fetch plugin config info by " + .. "plugin config id [" .. plugin_config_id .. "]: " + .. err} + end + + if res.status ~= 200 then + return nil, {error_msg = "failed to fetch plugin config info by " + .. "plugin config id [" .. plugin_config_id .. "], " + .. "response code: " .. res.status} + end + end + if conf.plugins then local ok, err = schema_plugin(conf.plugins) if not ok then diff --git a/apisix/cli/etcd.lua b/apisix/cli/etcd.lua index 0d41862b6414..5865f3990aa0 100644 --- a/apisix/cli/etcd.lua +++ b/apisix/cli/etcd.lua @@ -251,7 +251,7 @@ function _M.init(env, args) for _, dir_name in ipairs({"/routes", "/upstreams", "/services", "/plugins", "/consumers", "/node_status", "/ssl", "/global_rules", "/stream_routes", - "/proto", "/plugin_metadata"}) do + "/proto", "/plugin_metadata", "/plugin_configs"}) do local key = (etcd_conf.prefix or "") .. dir_name .. "/" diff --git a/apisix/init.lua b/apisix/init.lua index e7b0d1401bcc..4ea0b355c582 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 plugin_config = require("apisix.plugin_config") local script = require("apisix.script") local service_fetch = require("apisix.http.service").get local admin_init = require("apisix.admin.init") @@ -111,6 +112,7 @@ function _M.http_init_worker() router.http_init_worker() require("apisix.http.service").init_worker() plugin.init_worker() + plugin_config.init_worker() require("apisix.consumer").init_worker() if core.config == require("apisix.core.config_yaml") then @@ -381,6 +383,18 @@ function _M.http_access_phase() core.json.delay_encode(api_ctx.matched_route, true)) local enable_websocket = route.value.enable_websocket + + if route.value.plugin_config_id then + local pc = plugin_config.get(route.value.plugin_config_id) + if not pc then + core.log.error("failed to fetch plugin config by ", + "id: ", route.value.plugin_config_id) + return core.response.exit(503) + end + + route = plugin_config.merge(route, pc) + end + if route.value.service_id then local service = service_fetch(route.value.service_id) if not service then diff --git a/apisix/plugin_config.lua b/apisix/plugin_config.lua new file mode 100644 index 000000000000..13bba0e8a34d --- /dev/null +++ b/apisix/plugin_config.lua @@ -0,0 +1,69 @@ +-- +-- 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 plugin_checker = require("apisix.plugin").plugin_checker +local pairs = pairs +local error = error + + +local plugin_configs + + +local _M = { +} + + +function _M.init_worker() + local err + plugin_configs, err = core.config.new("/plugin_configs", { + automatic = true, + checker = plugin_checker, + }) + if not plugin_configs then + error("failed to sync /plugin_configs: " .. err) + end +end + + +function _M.get(id) + return plugin_configs:get(id) +end + + +function _M.merge(route_conf, plugin_config) + if route_conf.prev_plugin_config_ver ~= plugin_config.modifiedIndex then + if not route_conf.value.plugins then + route_conf.value.plugins = {} + elseif not route_conf.orig_plugins then + route_conf.orig_plugins = route_conf.value.plugins + route_conf.value.plugins = core.table.clone(route_conf.value.plugins) + end + + for name, value in pairs(plugin_config.value.plugins) do + route_conf.value.plugins[name] = value + end + + route_conf.update_count = route_conf.update_count + 1 + route_conf.modifiedIndex = route_conf.orig_modifiedIndex .. "#" .. route_conf.update_count + route_conf.prev_plugin_config_ver = plugin_config.modifiedIndex + end + + return route_conf +end + + +return _M diff --git a/apisix/plugins/example-plugin.lua b/apisix/plugins/example-plugin.lua index 83722ff93c5c..5d281ef1d803 100644 --- a/apisix/plugins/example-plugin.lua +++ b/apisix/plugins/example-plugin.lua @@ -73,7 +73,9 @@ end function _M.rewrite(conf, ctx) core.log.warn("plugin rewrite phase, conf: ", core.json.encode(conf)) - -- core.log.warn(" ctx: ", core.json.encode(ctx, true)) + core.log.warn("conf_type: ", ctx.conf_type) + core.log.warn("conf_id: ", ctx.conf_id) + core.log.warn("conf_version: ", ctx.conf_version) end diff --git a/apisix/router.lua b/apisix/router.lua index 036fa779967a..3afb3a48049d 100644 --- a/apisix/router.lua +++ b/apisix/router.lua @@ -28,6 +28,9 @@ local _M = {version = 0.3} local function filter(route) + route.orig_modifiedIndex = route.modifiedIndex + route.update_count = 0 + route.has_domain = false if not route.value then return diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 3404cc01005e..a951009fa1b1 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -467,6 +467,8 @@ _M.route = { script_id = id_schema, plugins = plugins_schema, + plugin_config_id = id_schema, + upstream = upstream_schema, labels = { @@ -544,7 +546,8 @@ _M.route = { }, ["not"] = { anyOf = { - {required = {"script", "plugins"}} + {required = {"script", "plugins"}}, + {required = {"script", "plugin_config_id"}}, } }, additionalProperties = false, @@ -746,6 +749,20 @@ _M.plugins = { } +_M.plugin_config = { + type = "object", + properties = { + id = id_schema, + desc = {type = "string", maxLength = 256}, + plugins = plugins_schema, + create_time = timestamp_def, + update_time = timestamp_def + }, + required = {"id", "plugins"}, + additionalProperties = false, +} + + _M.id_schema = id_schema diff --git a/doc/admin-api.md b/doc/admin-api.md index b007b8c67858..b2bccae78ca4 100644 --- a/doc/admin-api.md +++ b/doc/admin-api.md @@ -25,6 +25,7 @@ * [Upstream](#upstream) * [SSL](#ssl) * [Global Rule](#global-rule) +* [Plugin Config](#plugin-config) * [Plugin Metadata](#plugin-metadata) * [Plugin](#plugin) @@ -73,6 +74,7 @@ |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 || +|plugin_config_id|False, can't be used with `script` |Plugin|Binded plugin config object, see [Plugin Config](architecture-design.md#plugin-config) for more || |labels |False |Match Rules|Key/value pairs to specify attributes|{"version":"v2","build":"16","env":"production"}| |enable_websocket|False|Auxiliary| enable `websocket`(boolean), default `false`.|| |status |False|Auxiliary| enable this route, default `1`.|`1` to enable, `0` to disable| @@ -721,6 +723,34 @@ Config Example: |create_time|False|epoch timestamp in second, will be created automatically if missing | 1602883670| |update_time|False|epoch timestamp in second, will be created automatically if missing | 1602883670| +## Plugin config + +*API*:/apisix/admin/plugin_configs/{id} + +*Description*: Provide a group of plugins which can be reused across routes. + +> Request Methods: + +|Method |Request URI|Request Body|Description | +|---------|-------------------------|--|------| +|GET |/apisix/admin/plugin_configs|NULL|Fetch resource list| +|GET |/apisix/admin/plugin_configs/{id}|NULL|Fetch resource| +|PUT |/apisix/admin/plugin_configs/{id}|{...}|Create resource by ID| +|DELETE |/apisix/admin/plugin_configs/{id}|NULL|Remove resource| +|PATCH |/apisix/admin/plugin_configs/{id}|{...}|Standard PATCH. Update some attributes of the existing plugin config, and other attributes not involved will remain as they are; if you want to delete an attribute, set the value of the attribute Set to null to delete; especially, when the value of the attribute is an array, the attribute will be updated in full| +|PATCH |/apisix/admin/plugin_configs/{id}/{path}|{...}|SubPath PATCH, specify the attribute of plugin config to be updated through {path}, update the value of this attribute in full, and other attributes that are not involved will remain as they are.| + +> Request Body Parameters: + +|Parameter|Required|Description|Example| +|---------|---------|-----------|----| +|plugins |True |See [Plugin](architecture-design.md#plugin)|| +|desc |False|description, usage scenarios, and more.|customer xxxx| +|create_time|False|epoch timestamp in second, will be created automatically if missing | 1602883670| +|update_time|False|epoch timestamp in second, will be created automatically if missing | 1602883670| + +[Back to TOC](#table-of-contents) + ## Plugin Metadata *API*:/apisix/admin/plugin_metadata/{plugin_name} diff --git a/doc/architecture-design.md b/doc/architecture-design.md index fca94b107c87..529a078c5dde 100644 --- a/doc/architecture-design.md +++ b/doc/architecture-design.md @@ -31,6 +31,7 @@ - [**Router**](#router) - [**Consumer**](#consumer-1) - [**Global Rule**](#global-rule) +- [**Plugin Config**](#plugin-config) - [**Debug mode**](#debug-mode) ## APISIX @@ -614,6 +615,129 @@ curl https://{apisix_listen_address}/apisix/admin/global_rules [Back to top](#table-of-contents) +## Plugin Config + +To reuse common plugin configurations, you can exact them into a plugin config and +bind it with a route directly. + +For instance, you can do something like: + +```shell +# create a plugin config +$ curl http://127.0.0.1:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' +{ + "desc": "blah", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } +}' + +# bind it to route +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' +{ + "uris": ["/index.html"], + "plugin_config_id": 1, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +When we can't find the corresponding plugin config with the id, the requests hit the route will be terminated with HTTP status code 503. + +When a route already have `plugins` field configured, the `plugins` in the plugin config +will be merged into it. The same plugin in the plugin config will override one in the `plugins`. + +For example, + +``` +{ + "desc": "I am plugin_config 1", + "plugins": { + "ip-restriction": { + "whitelist": [ + "127.0.0.0/24", + "113.74.26.106" + ] + }, + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } +} +``` + ++ + +``` +{ + "uris": ["/index.html"], + "plugin_config_id": 1, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } + "plugins": { + "proxy-rewrite": { + "uri": "/test/add", + "scheme": "https", + "host": "apisix.iresty.com" + }, + "limit-count": { + "count": 20, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } +} +``` + += + +``` +{ + "uris": ["/index.html"], + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } + "plugins": { + "ip-restriction": { + "whitelist": [ + "127.0.0.0/24", + "113.74.26.106" + ] + }, + "proxy-rewrite": { + "uri": "/test/add", + "scheme": "https", + "host": "apisix.iresty.com" + }, + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } +} +``` + +[Back to top](#table-of-contents) + ## Debug mode ### Basic Debug Mode diff --git a/doc/zh-cn/admin-api.md b/doc/zh-cn/admin-api.md index 024550da8eb1..517dd68b0e2f 100644 --- a/doc/zh-cn/admin-api.md +++ b/doc/zh-cn/admin-api.md @@ -25,6 +25,7 @@ * [Upstream](#upstream) * [SSL](#ssl) * [Global Rule](#global-rule) +* [Plugin Config](#plugin-config) * [Plugin Metadata](#plugin-metadata) * [Plugin](#plugin) @@ -64,6 +65,7 @@ |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)|| +|plugin_config_id|可选,无法跟 script 一起配置|Plugin|绑定的 Plugin config 配置,详见 [Plugin config](architecture-design.md#plugin-config)|| |name |可选 |辅助 |标识路由名称|route-xxxx| |desc |可选 |辅助 |标识描述、使用场景等。|客户 xxxx| |host |可选 |匹配规则|当前请求域名,比如 `foo.com`;也支持泛域名,比如 `*.foo.com`。|"foo.com"| @@ -703,6 +705,8 @@ ssl 对象 json 配置内容: } ``` +[Back to TOC](#目录) + ## Global Rule *地址*:/apisix/admin/global_rules/{id} @@ -728,6 +732,36 @@ ssl 对象 json 配置内容: |create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670| |update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670| +[Back to TOC](#目录) + +## Plugin Config + +*地址*:/apisix/admin/plugin_configs/{id} + +*说明*:配置一组可以在路由间复用的插件。 + +> 请求方法: + +|名字 |请求 uri|请求 body|说明 | +|---------|-------------------------|--|------| +|GET |/apisix/admin/plugin_configs|无|获取资源列表| +|GET |/apisix/admin/plugin_configs/{id}|无|获取资源| +|PUT |/apisix/admin/plugin_configs/{id}|{...}|根据 id 创建资源| +|DELETE |/apisix/admin/plugin_configs/{id}|无|删除资源| +|PATCH |/apisix/admin/plugin_configs/{id}|{...}|标准 PATCH ,修改已有 Plugin Config 的部分属性,其他不涉及的属性会原样保留;如果你要删除某个属性,将该属性的值设置为null 即可删除;特别地,当需要修改属性的值为数组时,该属性将全量更新| +|PATCH |/apisix/admin/plugin_configs/{id}/{path}|{...}|SubPath PATCH,通过 {path} 指定 Plugin Config 要更新的属性,全量更新该属性的数据,其他不涉及的属性会原样保留。| + +> body 请求参数: + +|名字 |可选项 |类型 |说明 |示例| +|---------|---------|----|-----------|----| +|plugins |必需|Plugin|详见 [Plugin](architecture-design.md#plugin) || +|desc |可选|辅助|标识描述、使用场景等|customer xxxx| +|create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670| +|update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670| + +[Back to TOC](#目录) + ## Plugin Metadata *地址*:/apisix/admin/plugin_metadata/{plugin_name} diff --git a/doc/zh-cn/architecture-design.md b/doc/zh-cn/architecture-design.md index 3d6d2aba7379..c7e009b8de42 100644 --- a/doc/zh-cn/architecture-design.md +++ b/doc/zh-cn/architecture-design.md @@ -29,6 +29,7 @@ - [**Router**](#router) - [**Consumer**](#consumer-1) - [**Global Rule**](#global-rule) +- [**Plugin Config**](#plugin-config) - [**Debug mode**](#debug-mode) ## APISIX @@ -624,6 +625,128 @@ curl https://{apisix_listen_address}/apisix/admin/global_rules -H 'X-API-KEY: ed [返回目录](#目录) +## Plugin Config + +如果你想要复用一组通用的插件配置,你可以把它们提取成一个 Plugin config,并绑定到对应的路由上。 + +举个例子,你可以这么做: + +```shell +# 创建 Plugin config +$ curl http://127.0.0.1:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' +{ + "desc": "吾乃插件配置1", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } +}' + +# 绑定到路由上 +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' +{ + "uris": ["/index.html"], + "plugin_config_id": 1, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +如果找不到对应的 Plugin config,该路由上的请求会报 503 错误。 + +如果这个路由已经配置了 `plugins`,那么 Plugin config 里面的插件配置会合并进去。 +相同的插件会覆盖掉 `plugins` 原有的插件。 + +举个例子: + +``` +{ + "desc": "吾乃插件配置1", + "plugins": { + "ip-restriction": { + "whitelist": [ + "127.0.0.0/24", + "113.74.26.106" + ] + }, + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } +} +``` + ++ + +``` +{ + "uris": ["/index.html"], + "plugin_config_id": 1, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } + "plugins": { + "proxy-rewrite": { + "uri": "/test/add", + "scheme": "https", + "host": "apisix.iresty.com" + }, + "limit-count": { + "count": 20, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } +} +``` + += + +``` +{ + "uris": ["/index.html"], + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } + "plugins": { + "ip-restriction": { + "whitelist": [ + "127.0.0.0/24", + "113.74.26.106" + ] + }, + "proxy-rewrite": { + "uri": "/test/add", + "scheme": "https", + "host": "apisix.iresty.com" + }, + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } +} +``` + +[返回目录](#目录) + ## Debug mode ### 基本调试模式 diff --git a/t/admin/plugin-configs.t b/t/admin/plugin-configs.t new file mode 100644 index 000000000000..6bb70913b29c --- /dev/null +++ b/t/admin/plugin-configs.t @@ -0,0 +1,309 @@ +# +# 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(); +log_level("info"); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: PUT +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local etcd = require("apisix.core.etcd") + local code, body = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }]], + [[{ + "node": { + "value": { + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }, + "key": "/apisix/plugin_configs/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + + local res = assert(etcd.get('/plugin_configs/1')) + local create_time = res.body.node.value.create_time + assert(create_time ~= nil, "create_time is nil") + local update_time = res.body.node.value.update_time + assert(update_time ~= nil, "update_time is nil") + } + } +--- response_body +passed + + + +=== TEST 2: GET +--- 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_GET, + nil, + [[{ + "node": { + "value": { + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }, + "key": "/apisix/plugin_configs/1" + }, + "action": "get" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: GET all +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_configs', + ngx.HTTP_GET, + nil, + [[{ + "node": { + "dir": true, + "nodes": [ + { + "key": "/apisix/plugin_configs/1", + "value": { + "plugins": { + "limit-count": { + "time_window": 60, + "policy": "local", + "count": 2, + "key": "remote_addr", + "rejected_code": 503 + } + } + } + } + ], + "key": "/apisix/plugin_configs" + }, + "action": "get" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: PATCH +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local etcd = require("apisix.core.etcd") + local res = assert(etcd.get('/plugin_configs/1')) + local prev_create_time = res.body.node.value.create_time + assert(prev_create_time ~= nil, "create_time is nil") + local prev_update_time = res.body.node.value.update_time + assert(prev_update_time ~= nil, "update_time is nil") + ngx.sleep(1) + + local code, body = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PATCH, + [[{ + "plugins": { + "limit-count": { + "count": 3, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + }}]], + [[{ + "node": { + "value": { + "plugins": { + "limit-count": { + "count": 3, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }, + "key": "/apisix/plugin_configs/1" + }, + "action": "compareAndSwap" + }]] + ) + + ngx.status = code + ngx.say(body) + + local res = assert(etcd.get('/plugin_configs/1')) + local create_time = res.body.node.value.create_time + assert(prev_create_time == create_time, "create_time mismatched") + local update_time = res.body.node.value.update_time + assert(update_time ~= nil, "update_time is nil") + assert(prev_update_time ~= update_time, "update_time should be changed") + } + } +--- response_body +passed + + + +=== TEST 5: PATCH (sub path) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local etcd = require("apisix.core.etcd") + local res = assert(etcd.get('/plugin_configs/1')) + local prev_create_time = res.body.node.value.create_time + assert(prev_create_time ~= nil, "create_time is nil") + local prev_update_time = res.body.node.value.update_time + assert(prev_update_time ~= nil, "update_time is nil") + ngx.sleep(1) + + local code, body = t('/apisix/admin/plugin_configs/1/plugins', + ngx.HTTP_PATCH, + [[{ + "limit-count": { + "count": 3, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + }]], + [[{ + "node": { + "value": { + "plugins": { + "limit-count": { + "count": 3, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }, + "key": "/apisix/plugin_configs/1" + }, + "action": "compareAndSwap" + }]] + ) + + ngx.status = code + ngx.say(body) + + local res = assert(etcd.get('/plugin_configs/1')) + local create_time = res.body.node.value.create_time + assert(prev_create_time == create_time, "create_time mismatched") + local update_time = res.body.node.value.update_time + assert(update_time ~= nil, "update_time is nil") + assert(prev_update_time ~= update_time, "update_time should be changed") + } + } +--- response_body +passed + + + +=== TEST 6: invalid plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local etcd = require("apisix.core.etcd") + local code, body = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "rejected_code": 503, + "time_window": 60, + "key": "remote_addr" + } + } + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"} +--- error_code: 400 diff --git a/t/admin/routes2.t b/t/admin/routes2.t index fea3f623d492..9d76c940e75c 100644 --- a/t/admin/routes2.t +++ b/t/admin/routes2.t @@ -616,3 +616,37 @@ GET /t qr/invalid configuration: property \\"labels\\" validation failed/ --- no_error_log [error] + + + +=== TEST 17: route with plugin_config_id (not found) +--- 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, + [[{ + "methods": ["GET"], + "plugin_config_id": "not_found", + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }, + "uri": "/index.html" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"failed to fetch plugin config info by plugin config id [not_found], response code: 404"} +--- no_error_log +[error] diff --git a/t/config-center-yaml/plugin-configs.t b/t/config-center-yaml/plugin-configs.t new file mode 100644 index 000000000000..309388d72e4a --- /dev/null +++ b/t/config-center-yaml/plugin-configs.t @@ -0,0 +1,149 @@ +# +# 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); +log_level('info'); +no_root_location(); +no_shuffle(); +master_on(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $yaml_config = $block->yaml_config // <<_EOC_; +apisix: + node_listen: 1984 + config_center: yaml + enable_admin: false +_EOC_ + + $block->set_value("yaml_config", $yaml_config); + + if (!$block->request) { + $block->set_value("request", "GET /hello"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: sanity +--- apisix_yaml +plugin_configs: + - + id: 1 + plugins: + response-rewrite: + body: "hello\n" +routes: + - id: 1 + uri: /hello + plugin_config_id: 1 + upstream: + nodes: + "127.0.0.1:1980":1 + type: roundrobin +#END +--- response_body +hello + + + +=== TEST 2: plugin_config not found +--- apisix_yaml +routes: + - id: 1 + uri: /hello + plugin_config_id: 1 + upstream: + nodes: + "127.0.0.1:1980":1 + type: roundrobin +#END +--- error_code: 503 +--- error_log +failed to fetch plugin config by id: 1 + + + +=== TEST 3: mix plugins & plugin_config_id +--- apisix_yaml +plugin_configs: + - + id: 1 + plugins: + example-plugin: + i: 1 + response-rewrite: + body: "hello\n" +routes: + - id: 1 + uri: /echo + plugin_config_id: 1 + plugins: + proxy-rewrite: + headers: + "in": "out" + response-rewrite: + body: "world\n" + upstream: + nodes: + "127.0.0.1:1980":1 + type: roundrobin +#END +--- request +GET /echo +--- response_body +hello +--- response_headers +in: out +--- error_log eval +qr/conf_version: \d+#1,/ +--- no_error_log +[error] + + + +=== TEST 4: invalid plugin +--- apisix_yaml +plugin_configs: + - + id: 1 + plugins: + example-plugin: + skey: "s" + response-rewrite: + body: "hello\n" +routes: + - id: 1 + uri: /hello + plugin_config_id: 1 + upstream: + nodes: + "127.0.0.1:1980":1 + type: roundrobin +#END +--- error_code: 503 +--- error_log +failed to check the configuration of plugin example-plugin +failed to fetch plugin config by id: 1 diff --git a/t/config-center-yaml/route.t b/t/config-center-yaml/route.t index 55653a47837c..e77cf732f3f8 100644 --- a/t/config-center-yaml/route.t +++ b/t/config-center-yaml/route.t @@ -281,3 +281,25 @@ GET /hello hello world --- no_error_log [error] + + + +=== TEST 11: script with plugin_config_id +--- yaml_config eval: $::yaml_config +--- apisix_yaml +routes: + - + id: 1 + uri: /hello + script: "local ngx = ngx" + plugin_config_id: "1" + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +--- request +GET /hello +--- error_code: 404 +--- error_log +failed to check item data of [routes] diff --git a/t/lib/test_admin.lua b/t/lib/test_admin.lua index 184d370d74bf..56f9db8e69a8 100644 --- a/t/lib/test_admin.lua +++ b/t/lib/test_admin.lua @@ -131,13 +131,12 @@ function _M.comp_tab(left_tab, right_tab) end -function _M.set_config_yaml(data) - local fn +local function set_yaml(fn, data) local profile = os.getenv("APISIX_PROFILE") if profile then - fn = "config-" .. profile .. ".yaml" + fn = fn .. "-" .. profile .. ".yaml" else - fn = "config.yaml" + fn = fn .. ".yaml" end local f = assert(io.open(os.getenv("TEST_NGINX_HTML_DIR") .. "/../conf/" .. fn, 'w')) @@ -146,6 +145,16 @@ function _M.set_config_yaml(data) end +function _M.set_config_yaml(data) + set_yaml("config", data) +end + + +function _M.set_apisix_yaml(data) + set_yaml("apisix", data) +end + + function _M.test(uri, method, body, pattern, headers) if not headers then headers = {} diff --git a/t/node/plugin-configs.t b/t/node/plugin-configs.t new file mode 100644 index 000000000000..fe79bb98d136 --- /dev/null +++ b/t/node/plugin-configs.t @@ -0,0 +1,125 @@ +# +# 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); +log_level('info'); +no_root_location(); +no_shuffle(); +master_on(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: change plugin config will cause the conf_version change +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, err = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "response-rewrite": { + "body": "hello" + } + } + }]] + ) + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + + local code, err = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugin_config_id": 1, + "plugins": { + "example-plugin": { + "i": 1 + } + } + }]] + ) + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + ngx.sleep(0.1) + + local code, err, org_body = t('/hello') + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + ngx.say(org_body) + + local code, err = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PATCH, + [[{ + "plugins": { + "response-rewrite": { + "body": "world" + } + } + }]] + ) + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + ngx.sleep(0.1) + + local code, err, org_body = t('/hello') + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + ngx.say(org_body) + } + } +--- response_body +hello +world +--- grep_error_log eval +qr/conf_version: \d+#\d/ +--- grep_error_log_out eval +qr/conf_version: \d+#1 +conf_version: \d+#2 +/ diff --git a/t/plugin/example.t b/t/plugin/example.t index 7149b1ded691..6b72eae4b700 100644 --- a/t/plugin/example.t +++ b/t/plugin/example.t @@ -150,11 +150,13 @@ done end local encode_json = require("toolkit.json").encode + local conf = {} + local ctx = {} for _, plugin in ipairs(plugins) do ngx.say("plugin name: ", plugin.name, " priority: ", plugin.priority) - plugin.rewrite() + plugin.rewrite(conf, ctx) end } }