From 1d5e7cccf229825a2e483707c3fd19f93042280d Mon Sep 17 00:00:00 2001 From: "Ling Samuel (WSL)" Date: Tue, 11 Jul 2023 09:16:20 +0800 Subject: [PATCH 1/2] feat: support force delete resource Signed-off-by: Ling Samuel (WSL) --- apisix/admin/init.lua | 6 + apisix/admin/proto.lua | 2 +- apisix/admin/resource.lua | 2 +- docs/en/latest/admin-api.md | 27 ++++ docs/zh/latest/admin-api.md | 27 ++++ t/admin/consumer-group-force-delete.t | 169 ++++++++++++++++++++++++ t/admin/plugin-configs-force-delete.t | 169 ++++++++++++++++++++++++ t/admin/protos-force-delete.t | 181 ++++++++++++++++++++++++++ t/admin/services-force-delete.t | 162 +++++++++++++++++++++++ t/admin/upstream-force-delete.t | 160 +++++++++++++++++++++++ 10 files changed, 903 insertions(+), 2 deletions(-) create mode 100644 t/admin/consumer-group-force-delete.t create mode 100644 t/admin/plugin-configs-force-delete.t create mode 100644 t/admin/protos-force-delete.t create mode 100644 t/admin/services-force-delete.t create mode 100644 t/admin/upstream-force-delete.t diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua index 412e15447f30..21f3868f1bce 100644 --- a/apisix/admin/init.lua +++ b/apisix/admin/init.lua @@ -221,6 +221,12 @@ local function run() if seg_res == "schema" or seg_res == "plugins" then code, data = resource[method](seg_id, req_body, seg_sub_path, uri_args) else + if method == "delete" then + if not req_body then + req_body = {} + end + req_body._force_delete = ngx.req.get_headers()["X-Force-Delete"] + end code, data = resource[method](resource, seg_id, req_body, seg_sub_path, uri_args) end diff --git a/apisix/admin/proto.lua b/apisix/admin/proto.lua index de4d24e23e88..f8133cc80b71 100644 --- a/apisix/admin/proto.lua +++ b/apisix/admin/proto.lua @@ -50,7 +50,7 @@ local function check_proto_used(plugins, deleting, ptype, pid) if type(plugins) == "table" and plugins["grpc-transcode"] and plugins["grpc-transcode"].proto_id and tostring(plugins["grpc-transcode"].proto_id) == deleting then - return false, {error_msg = "can not delete this proto," + return false, {error_msg = "can not delete this proto, " .. ptype .. " [" .. pid .. "] is still using it now"} end diff --git a/apisix/admin/resource.lua b/apisix/admin/resource.lua index bfc6789df0fb..4b25a7c1c3ae 100644 --- a/apisix/admin/resource.lua +++ b/apisix/admin/resource.lua @@ -253,7 +253,7 @@ function _M:delete(id, conf, sub_path) key = key .. "/" .. id - if self.delete_checker then + if self.delete_checker and conf._force_delete ~= "true" then local code, err = self.delete_checker(id) if err then return code, err diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index 74049f9ef057..8d952e9caab9 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -103,6 +103,33 @@ deployment: This will find the environment variable `ADMIN_KEY` first, and if it does not exist, it will use `edd1c9f034335f136f87ad84b625c8f1` as the default value. +### Force Delete + +By default, the Admin API checks for references between resources and will refuse to delete resources in use. + +You can make a force deletion by adding the request header `X-Force-Delete: true` to the delete request, for example: + +```bash +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" +}' +$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ + "uri": "/*", + "upstream_id": 1 +}' +{"value":{"priority":0,"upstream_id":1,"uri":"/*","create_time":1689038794,"id":"1","status":1,"update_time":1689038916},"key":"/apisix/routes/1"} + +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE +{"error_msg":"can not delete this upstream, route [1] is still using it now"} +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: any-value' +{"error_msg":"can not delete this upstream, route [1] is still using it now"} +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: true' +{"deleted":"1","key":"/apisix/upstreams/1"} +``` + ## V3 new feature The Admin API has made some breaking changes in V3 version, as well as supporting additional features. diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index ad6e2ff64132..9cacbb1aa83e 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -105,6 +105,33 @@ deployment: 首先查找环境变量 `ADMIN_KEY`,如果该环境变量不存在,它将使用 `edd1c9f034335f136f87ad84b625c8f1` 作为默认值。 +### 强制删除 {#force-delete} + +默认情况下,Admin API 会检查资源间的引用关系,将会拒绝删除正在使用中的资源。 + +可以通过在删除请求中添加请求头 `X-Force-Delete: true` 来进行强制删除,例如: + +```bash +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" +}' +$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ + "uri": "/*", + "upstream_id": 1 +}' +{"value":{"priority":0,"upstream_id":1,"uri":"/*","create_time":1689038794,"id":"1","status":1,"update_time":1689038916},"key":"/apisix/routes/1"} + +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE +{"error_msg":"can not delete this upstream, route [1] is still using it now"} +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: any-value' +{"error_msg":"can not delete this upstream, route [1] is still using it now"} +$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: true' +{"deleted":"1","key":"/apisix/upstreams/1"} +``` + ## v3 版本新功能 {#v3-new-function} 在 APISIX v3 版本中,Admin API 支持了一些不向下兼容的新特性,比如支持新的响应体格式、支持分页查询、支持过滤资源等。 diff --git a/t/admin/consumer-group-force-delete.t b/t/admin/consumer-group-force-delete.t new file mode 100644 index 000000000000..2e5d65a1e4b7 --- /dev/null +++ b/t/admin/consumer-group-force-delete.t @@ -0,0 +1,169 @@ +# +# 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->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set consumer_group(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumer_groups/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 200, + "time_window": 60, + "rejected_code": 503, + "group": "$consumer_group_id" + } + } + }]] + ) + ngx.status = code + ngx.say(body) + } + } +--- error_code: 201 +--- response_body +passed + + + +=== TEST 2: add consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/consumers/1', + ngx.HTTP_PUT, + [[{ + "username": "1", + "plugins": { + "key-auth": { + "key": "auth-one" + } + }, + "group_id": "1" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.print(message) + return + end + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 3: delete consumer_group(wrong header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/consumer_groups/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "any-value", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this consumer group, consumer [1] is still using it now"} + + + +=== TEST 4: delete consumer_group(without force delete header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/consumer_groups/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this consumer group, consumer [1] is still using it now"} + + + +=== TEST 5: delete consumer_group(force delete) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/consumer_groups/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "true", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed + + + +=== TEST 6: delete consumer +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/consumers/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed diff --git a/t/admin/plugin-configs-force-delete.t b/t/admin/plugin-configs-force-delete.t new file mode 100644 index 000000000000..ded0fb1f10bf --- /dev/null +++ b/t/admin/plugin-configs-force-delete.t @@ -0,0 +1,169 @@ +# +# 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->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set plugin_configs(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503 + } + } + }]] + ) + ngx.status = code + ngx.say(body) + } + } +--- error_code: 201 +--- response_body +passed + + + +=== TEST 2: add route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugin_config_id": 1, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/index.html" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.print(message) + return + end + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 3: delete plugin_configs(wrong header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "any-value", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this plugin config, route [1] is still using it now"} + + + +=== TEST 4: delete plugin_configs(without force delete header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this plugin config, route [1] is still using it now"} + + + +=== TEST 5: delete plugin_configs(force delete) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "true", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed + + + +=== TEST 6: delete route +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed diff --git a/t/admin/protos-force-delete.t b/t/admin/protos-force-delete.t new file mode 100644 index 000000000000..284b8cc1172a --- /dev/null +++ b/t/admin/protos-force-delete.t @@ -0,0 +1,181 @@ +# +# 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->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set proto(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/protos/1', + ngx.HTTP_PUT, + [[{ + "content" : "syntax = \"proto3\"; + package helloworld; + service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + } + message HelloRequest { + string name = 1; + } + message HelloReply { + string message = 1; + }" + }]] + ) + ngx.status = code + ngx.say(body) + } + } +--- error_code: 201 +--- response_body +passed + + + +=== TEST 2: add route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "uri": "/grpctest", + "plugins": { + "grpc-transcode": { + "proto_id": "1", + "service": "helloworld.Greeter", + "method": "SayHello" + } + }, + "upstream": { + "scheme": "grpc", + "type": "roundrobin", + "nodes": { + "127.0.0.1:50051": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.print(message) + return + end + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 3: delete proto(wrong header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/protos/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "any-value", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this proto, route [1] is still using it now"} + + + +=== TEST 4: delete proto(without force delete header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/protos/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this proto, route [1] is still using it now"} + + + +=== TEST 5: delete proto(force delete) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/protos/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "true", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed + + + +=== TEST 6: delete route +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed diff --git a/t/admin/services-force-delete.t b/t/admin/services-force-delete.t new file mode 100644 index 000000000000..6ef046403e6e --- /dev/null +++ b/t/admin/services-force-delete.t @@ -0,0 +1,162 @@ +# +# 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->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set service(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + } + }]] + ) + ngx.status = code + ngx.say(body) + } + } +--- error_code: 201 +--- response_body +passed + + + +=== TEST 2: add route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "service_id": 1, + "uri": "/index.html" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.print(message) + return + end + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 3: delete service(wrong header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/services/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "any-value", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this service directly, route [1] is still using it now"} + + + +=== TEST 4: delete service(without force delete header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/services/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this service directly, route [1] is still using it now"} + + + +=== TEST 5: delete service(force delete) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/services/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "true", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed + + + +=== TEST 6: delete route +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed diff --git a/t/admin/upstream-force-delete.t b/t/admin/upstream-force-delete.t new file mode 100644 index 000000000000..306a124482b5 --- /dev/null +++ b/t/admin/upstream-force-delete.t @@ -0,0 +1,160 @@ +# +# 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->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set upstream(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }]] + ) + ngx.status = code + ngx.say(body) + } + } +--- error_code: 201 +--- response_body +passed + + + +=== TEST 2: add route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream_id": 1, + "uri": "/index.html" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.print(message) + return + end + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 3: delete upstream(wrong header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/upstreams/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "any-value", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this upstream, route [1] is still using it now"} + + + +=== TEST 4: delete upstream(without force delete header) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/upstreams/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body +[delete] code: 400 message: {"error_msg":"can not delete this upstream, route [1] is still using it now"} + + + +=== TEST 5: delete upstream(force delete) +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/upstreams/1', + ngx.HTTP_DELETE, nil, nil, + { + ["X-Force-Delete"] = "true", + } + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed + + + +=== TEST 6: delete route +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.3) + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/routes/1', + ngx.HTTP_DELETE + ) + ngx.print("[delete] code: ", code, " message: ", message) + } + } +--- response_body chomp +[delete] code: 200 message: passed From c949ba2c11f85c4e4fffb936ed8fb7b568835615 Mon Sep 17 00:00:00 2001 From: "Ling Samuel (WSL)" Date: Fri, 14 Jul 2023 10:46:53 +0800 Subject: [PATCH 2/2] -m Signed-off-by: Ling Samuel (WSL) --- apisix/admin/init.lua | 6 ------ apisix/admin/resource.lua | 4 ++-- docs/en/latest/admin-api.md | 6 +++--- docs/zh/latest/admin-api.md | 6 +++--- t/admin/consumer-group-force-delete.t | 14 ++++---------- t/admin/plugin-configs-force-delete.t | 14 ++++---------- t/admin/protos-force-delete.t | 14 ++++---------- t/admin/services-force-delete.t | 14 ++++---------- t/admin/upstream-force-delete.t | 14 ++++---------- 9 files changed, 28 insertions(+), 64 deletions(-) diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua index 21f3868f1bce..412e15447f30 100644 --- a/apisix/admin/init.lua +++ b/apisix/admin/init.lua @@ -221,12 +221,6 @@ local function run() if seg_res == "schema" or seg_res == "plugins" then code, data = resource[method](seg_id, req_body, seg_sub_path, uri_args) else - if method == "delete" then - if not req_body then - req_body = {} - end - req_body._force_delete = ngx.req.get_headers()["X-Force-Delete"] - end code, data = resource[method](resource, seg_id, req_body, seg_sub_path, uri_args) end diff --git a/apisix/admin/resource.lua b/apisix/admin/resource.lua index 4b25a7c1c3ae..35fe3bba2476 100644 --- a/apisix/admin/resource.lua +++ b/apisix/admin/resource.lua @@ -230,7 +230,7 @@ function _M:put(id, conf, sub_path, args) end -- Keep the unused conf to make the args list consistent with other methods -function _M:delete(id, conf, sub_path) +function _M:delete(id, conf, sub_path, uri_args) if core.table.array_find(self.unsupported_methods, "delete") then return 405, {error_msg = "not supported `DELETE` method for " .. self.kind} end @@ -253,7 +253,7 @@ function _M:delete(id, conf, sub_path) key = key .. "/" .. id - if self.delete_checker and conf._force_delete ~= "true" then + if self.delete_checker and uri_args.force ~= "true" then local code, err = self.delete_checker(id) if err then return code, err diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index 8d952e9caab9..d9c077a11acd 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -107,7 +107,7 @@ This will find the environment variable `ADMIN_KEY` first, and if it does not ex By default, the Admin API checks for references between resources and will refuse to delete resources in use. -You can make a force deletion by adding the request header `X-Force-Delete: true` to the delete request, for example: +You can make a force deletion by adding the request argument `force=true` to the delete request, for example: ```bash $ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ @@ -124,9 +124,9 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f $ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE {"error_msg":"can not delete this upstream, route [1] is still using it now"} -$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: any-value' +$ curl "http://127.0.0.1:9180/apisix/admin/upstreams/1?force=anyvalue" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE {"error_msg":"can not delete this upstream, route [1] is still using it now"} -$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: true' +$ curl "http://127.0.0.1:9180/apisix/admin/upstreams/1?force=true" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE {"deleted":"1","key":"/apisix/upstreams/1"} ``` diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 9cacbb1aa83e..d3cf714b89fe 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -109,7 +109,7 @@ deployment: 默认情况下,Admin API 会检查资源间的引用关系,将会拒绝删除正在使用中的资源。 -可以通过在删除请求中添加请求头 `X-Force-Delete: true` 来进行强制删除,例如: +可以通过在删除请求中添加请求参数 `force=true` 来进行强制删除,例如: ```bash $ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ @@ -126,9 +126,9 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f $ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE {"error_msg":"can not delete this upstream, route [1] is still using it now"} -$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: any-value' +$ curl "http://127.0.0.1:9180/apisix/admin/upstreams/1?force=anyvalue" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE {"error_msg":"can not delete this upstream, route [1] is still using it now"} -$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -H 'X-Force-Delete: true' +$ curl "http://127.0.0.1:9180/apisix/admin/upstreams/1?force=true" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE {"deleted":"1","key":"/apisix/upstreams/1"} ``` diff --git a/t/admin/consumer-group-force-delete.t b/t/admin/consumer-group-force-delete.t index 2e5d65a1e4b7..4b2fb2d09a67 100644 --- a/t/admin/consumer-group-force-delete.t +++ b/t/admin/consumer-group-force-delete.t @@ -102,11 +102,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/consumer_groups/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "any-value", - } + local code, message = t('/apisix/admin/consumer_groups/1?force=anyvalue', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } @@ -139,11 +136,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/consumer_groups/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "true", - } + local code, message = t('/apisix/admin/consumer_groups/1?force=true', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } diff --git a/t/admin/plugin-configs-force-delete.t b/t/admin/plugin-configs-force-delete.t index ded0fb1f10bf..7d4f73739e34 100644 --- a/t/admin/plugin-configs-force-delete.t +++ b/t/admin/plugin-configs-force-delete.t @@ -102,11 +102,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/plugin_configs/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "any-value", - } + local code, message = t('/apisix/admin/plugin_configs/1?force=anyvalue', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } @@ -139,11 +136,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/plugin_configs/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "true", - } + local code, message = t('/apisix/admin/plugin_configs/1?force=true', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } diff --git a/t/admin/protos-force-delete.t b/t/admin/protos-force-delete.t index 284b8cc1172a..909128924bfe 100644 --- a/t/admin/protos-force-delete.t +++ b/t/admin/protos-force-delete.t @@ -114,11 +114,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/protos/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "any-value", - } + local code, message = t('/apisix/admin/protos/1?force=anyvalue', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } @@ -151,11 +148,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/protos/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "true", - } + local code, message = t('/apisix/admin/protos/1?force=true', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } diff --git a/t/admin/services-force-delete.t b/t/admin/services-force-delete.t index 6ef046403e6e..439b44e098ab 100644 --- a/t/admin/services-force-delete.t +++ b/t/admin/services-force-delete.t @@ -95,11 +95,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/services/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "any-value", - } + local code, message = t('/apisix/admin/services/1?force=anyvalue', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } @@ -132,11 +129,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/services/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "true", - } + local code, message = t('/apisix/admin/services/1?force=true', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } diff --git a/t/admin/upstream-force-delete.t b/t/admin/upstream-force-delete.t index 306a124482b5..6d834b11469a 100644 --- a/t/admin/upstream-force-delete.t +++ b/t/admin/upstream-force-delete.t @@ -93,11 +93,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/upstreams/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "any-value", - } + local code, message = t('/apisix/admin/upstreams/1?force=anyvalue', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) } @@ -130,11 +127,8 @@ passed content_by_lua_block { ngx.sleep(0.3) local t = require("lib.test_admin").test - local code, message = t('/apisix/admin/upstreams/1', - ngx.HTTP_DELETE, nil, nil, - { - ["X-Force-Delete"] = "true", - } + local code, message = t('/apisix/admin/upstreams/1?force=true', + ngx.HTTP_DELETE ) ngx.print("[delete] code: ", code, " message: ", message) }