This is an automated email from the ASF dual-hosted git repository.
monkeydluffy 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 e1712368e feat: add schema validate admin API (#10065)
e1712368e is described below
commit e1712368e933087fb136be96902be2864d3c56ea
Author: jinhua luo <[email protected]>
AuthorDate: Tue Aug 22 12:02:24 2023 +0800
feat: add schema validate admin API (#10065)
---
apisix/admin/init.lua | 40 +++++
docs/en/latest/admin-api.md | 49 ++++++
t/admin/schema-validate.t | 400 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 489 insertions(+)
diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua
index 0d4ef9323..333c798e6 100644
--- a/apisix/admin/init.lua
+++ b/apisix/admin/init.lua
@@ -376,6 +376,41 @@ local function reload_plugins(data, event, source, pid)
end
+local function schema_validate()
+ local uri_segs = core.utils.split_uri(ngx.var.uri)
+ core.log.info("uri: ", core.json.delay_encode(uri_segs))
+
+ local seg_res = uri_segs[6]
+ local resource = resources[seg_res]
+ if not resource then
+ core.response.exit(404, {error_msg = "Unsupported resource type: "..
seg_res})
+ end
+
+ local req_body, err = core.request.get_body(MAX_REQ_BODY)
+ if err then
+ core.log.error("failed to read request body: ", err)
+ core.response.exit(400, {error_msg = "invalid request body: " .. err})
+ end
+
+ if req_body then
+ local data, err = core.json.decode(req_body)
+ if err then
+ core.log.error("invalid request body: ", req_body, " err: ", err)
+ core.response.exit(400, {error_msg = "invalid request body: " ..
err,
+ req_body = req_body})
+ end
+
+ req_body = data
+ end
+
+ local ok, err = core.schema.check(resource.schema, req_body)
+ if ok then
+ core.response.exit(200)
+ end
+ core.response.exit(400, {error_msg = err})
+end
+
+
local uri_route = {
{
paths = [[/apisix/admin]],
@@ -392,6 +427,11 @@ local uri_route = {
methods = {"GET"},
handler = get_plugins_list,
},
+ {
+ paths = [[/apisix/admin/schema/validate/*]],
+ methods = {"POST"},
+ handler = schema_validate,
+ },
{
paths = reload_event,
methods = {"PUT"},
diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md
index e34468eac..e0f58fd5f 100644
--- a/docs/en/latest/admin-api.md
+++ b/docs/en/latest/admin-api.md
@@ -1522,3 +1522,52 @@ Proto resource request address: /apisix/admin/protos/{id}
| content | True | String | content of `.proto` or `.pb` files | See
[here](./plugins/grpc-transcode.md#enabling-the-plugin) |
| create_time | False | Epoch timestamp (in seconds) of the created time.
If missing, this field will be populated automatically. |
1602883670 |
| update_time | False | Epoch timestamp (in seconds) of the updated time.
If missing, this field will be populated automatically. |
1602883670 |
+
+## Schema validation
+
+Check the validity of a configuration against its entity schema. This allows
you to test your input before submitting a request to the entity endpoints of
the Admin API.
+
+Note that this only performs the schema validation checks, checking that the
input configuration is well-formed. Requests to the entity endpoint using the
given configuration may still fail due to other reasons, such as invalid
foreign key relationships or uniqueness check failures against the contents of
the data store.
+
+### Schema validation
+
+Schema validation request address: /apisix/admin/schema/validate/{resource}
+
+### Request Methods
+
+| Method | Request URI | Request Body | Description
|
+| ------ | -------------------------------- | ------------ |
----------------------------------------------- |
+| POST | /apisix/admin/schema/validate/{resource} | {..resource conf..}
| Validate the resource configuration against corresponding schema.
|
+
+### Request Body Parameters
+
+* 200: validate ok.
+* 400: validate failed, with error as response body in JSON format.
+
+Example:
+
+```bash
+curl http://127.0.0.1:9180/apisix/admin/schema/validate/routes \
+ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X POST -i -d '{
+ "uri": 1980,
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+}'
+HTTP/1.1 400 Bad Request
+Date: Mon, 21 Aug 2023 07:37:13 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+Server: APISIX/3.4.0
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Credentials: true
+Access-Control-Expose-Headers: *
+Access-Control-Max-Age: 3600
+
+{"error_msg":"property \"uri\" validation failed: wrong type: expected string,
got number"}
+```
diff --git a/t/admin/schema-validate.t b/t/admin/schema-validate.t
new file mode 100644
index 000000000..46f51021e
--- /dev/null
+++ b/t/admin/schema-validate.t
@@ -0,0 +1,400 @@
+#
+# 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("warn");
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: validate ok
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "uri": "/httpbin/*",
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 200
+
+
+
+=== TEST 2: validate failed, wrong uri type
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "uri": 666,
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg": {"property \"uri\" validation failed: wrong type: expected
string, got number"}}
+
+
+
+=== TEST 3: validate failed, length limit
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "uri": "",
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"property \"uri\" validation failed: string too short, expected
at least 1, got 0"}
+
+
+
+=== TEST 4: validate failed, array type expected
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "uris": "foobar",
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"property \"uris\" validation failed: wrong type: expected array,
got string"}
+
+
+
+=== TEST 5: validate failed, array size limit
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "uris": [],
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"property \"uris\" validation failed: expect array to have at
least 1 items"}
+
+
+
+=== TEST 6: validate failed, array unique items
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "uris": ["/foo", "/foo"],
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"property \"uris\" validation failed: expected unique items but
items 1 and 2 are equal"}
+
+
+
+=== TEST 7: validate failed, uri or uris is mandatory
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"allOf 1 failed: value should match only one schema, but matches
none"}
+
+
+
+=== TEST 8: validate failed, enum check
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "status": 3,
+ "uri": "/foo",
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"property \"status\" validation failed: matches none of the enum
values"}
+
+
+
+=== TEST 9: validate failed, wrong combination
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "script": "xxxxxxxxxxxxxxxxxxxxx",
+ "plugin_config_id": "foo"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"allOf 1 failed: value should match only one schema, but matches
none"}
+
+
+
+=== TEST 10: validate failed, id_schema check
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/routes',
+ ngx.HTTP_POST,
+ [[{
+ "plugin_config_id": "@@@@@@@@@@@@@@@@",
+ "uri": "/foo",
+ "upstream": {
+ "scheme": "https",
+ "type": "roundrobin",
+ "nodes": {
+ "nghttp2.org": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"property \"plugin_config_id\" validation failed: object matches
none of the required"}
+
+
+
+=== TEST 11: upstream ok
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/upstreams',
+ ngx.HTTP_POST,
+ [[{
+ "nodes":{
+ "nghttp2.org":100
+ },
+ "type":"roundrobin"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 200
+
+
+
+=== TEST 12: upstream failed, wrong nodes format
+--- config
+location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/schema/validate/upstreams',
+ ngx.HTTP_POST,
+ [[{
+ "nodes":[
+ "nghttp2.org"
+ ],
+ "type":"roundrobin"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ }
+}
+--- error_code: 400
+--- response
+{"error_msg":"allOf 1 failed: value should match only one schema, but matches
none"}