-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat: add validate API to standalone mode #12718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Revolyssup
wants to merge
6
commits into
apache:master
Choose a base branch
from
Revolyssup:revolyssup/standalone-validate
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+454
−32
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| -- | ||
| -- 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. | ||
|
|
@@ -22,6 +21,7 @@ local str_find = string.find | |
| local str_sub = string.sub | ||
| local tostring = tostring | ||
| local ngx = ngx | ||
| local pcall = pcall | ||
| local ngx_time = ngx.time | ||
| local get_method = ngx.req.get_method | ||
| local shared_dict = ngx.shared["standalone-config"] | ||
|
|
@@ -158,6 +158,128 @@ local function check_conf(checker, schema, item, typ) | |
| end | ||
|
|
||
|
|
||
| local function validate_configuration(req_body, collect_all_errors) | ||
| local validation_results = { | ||
| valid = true, | ||
| errors = {} | ||
| } | ||
|
|
||
| for key, conf_version_key in pairs(ALL_RESOURCE_KEYS) do | ||
| local items = req_body[key] | ||
| local resource = resources[key] or {} | ||
|
|
||
| -- Validate conf_version_key if present | ||
| local new_conf_version = req_body[conf_version_key] | ||
| if new_conf_version and type(new_conf_version) ~= "number" then | ||
| local error_msg | ||
| if collect_all_errors then | ||
| error_msg = conf_version_key .. " must be a number, got " .. type(new_conf_version) | ||
| else | ||
| error_msg = conf_version_key .. " must be a number" | ||
| end | ||
|
|
||
| if not collect_all_errors then | ||
| return false, error_msg | ||
| end | ||
| validation_results.valid = false | ||
| table_insert(validation_results.errors, { | ||
| resource_type = key, | ||
| error = error_msg | ||
| }) | ||
| end | ||
|
|
||
| if items and #items > 0 then | ||
| local item_schema = resource.schema | ||
| local item_checker = resource.checker | ||
| local id_set = {} | ||
|
|
||
| for index, item in ipairs(items) do | ||
| local item_temp = tbl_deepcopy(item) | ||
| local valid, err = check_conf(item_checker, item_schema, item_temp, key) | ||
| if not valid then | ||
| local err_prefix = "invalid " .. key .. " at index " .. (index - 1) .. ", err: " | ||
| local err_msg = type(err) == "table" and err.error_msg or err | ||
| local error_msg = err_prefix .. err_msg | ||
|
|
||
| if not collect_all_errors then | ||
| return false, error_msg | ||
| end | ||
| validation_results.valid = false | ||
| table_insert(validation_results.errors, { | ||
| resource_type = key, | ||
| index = index - 1, | ||
| error = error_msg | ||
| }) | ||
| end | ||
|
|
||
| -- check for duplicate IDs | ||
| local duplicated, dup_err = check_duplicate(item, key, id_set) | ||
| if duplicated then | ||
| if not collect_all_errors then | ||
| return false, dup_err | ||
| end | ||
| validation_results.valid = false | ||
| table_insert(validation_results.errors, { | ||
| resource_type = key, | ||
| index = index - 1, | ||
| error = dup_err | ||
| }) | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| if collect_all_errors then | ||
| return validation_results.valid, validation_results | ||
| else | ||
| return validation_results.valid, nil | ||
| end | ||
| end | ||
|
|
||
| local function validate(ctx) | ||
| local content_type = core.request.header(nil, "content-type") or "application/json" | ||
| local req_body, err = core.request.get_body() | ||
| if err then | ||
| return core.response.exit(400, {error_msg = "invalid request body: " .. err}) | ||
| end | ||
|
|
||
| if not req_body or #req_body <= 0 then | ||
| return core.response.exit(400, {error_msg = "invalid request body: empty request body"}) | ||
| end | ||
|
|
||
| local data | ||
| if core.string.has_prefix(content_type, "application/yaml") then | ||
| local ok, result = pcall(yaml.load, req_body, { all = false }) | ||
| if not ok or type(result) ~= "table" then | ||
| err = "invalid yaml request body" | ||
| else | ||
| data = result | ||
| end | ||
| else | ||
| data, err = core.json.decode(req_body) | ||
| end | ||
|
|
||
| if err then | ||
| core.log.error("invalid request body: ", req_body, " err: ", err) | ||
| return core.response.exit(400, {error_msg = "invalid request body: " .. err}) | ||
| end | ||
|
|
||
| local valid, validation_results = validate_configuration(data, true) | ||
|
|
||
| if valid then | ||
| return core.response.exit(200, { | ||
| message = "Configuration is valid", | ||
| valid = true | ||
| }) | ||
| else | ||
| return core.response.exit(400, { | ||
| error_msg = "Configuration validation failed", | ||
| valid = false, | ||
| errors = validation_results.errors | ||
| }) | ||
| end | ||
| end | ||
|
|
||
| local function update(ctx) | ||
| -- check digest header existence | ||
| local digest = core.request.header(nil, METADATA_DIGEST) | ||
|
|
@@ -211,54 +333,44 @@ local function update(ctx) | |
| return core.response.exit(204) | ||
| end | ||
|
|
||
| -- check input by jsonschema | ||
| local valid, error_msg = validate_configuration(req_body, false) | ||
| if not valid then | ||
| return core.response.exit(400, { error_msg = error_msg }) | ||
| end | ||
|
|
||
| -- check input by jsonschema and build the final config | ||
| local apisix_yaml = {} | ||
|
|
||
| for key, conf_version_key in pairs(ALL_RESOURCE_KEYS) do | ||
| local conf_version = config and config[conf_version_key] or 0 | ||
| local items = req_body[key] | ||
| local new_conf_version = req_body[conf_version_key] | ||
| local resource = resources[key] or {} | ||
| if not new_conf_version then | ||
| new_conf_version = conf_version + 1 | ||
| else | ||
| if type(new_conf_version) ~= "number" then | ||
| return core.response.exit(400, { | ||
| error_msg = conf_version_key .. " must be a number", | ||
| }) | ||
| end | ||
|
|
||
| if new_conf_version then | ||
| if new_conf_version < conf_version then | ||
| return core.response.exit(400, { | ||
| error_msg = conf_version_key .. | ||
| " must be greater than or equal to (" .. conf_version .. ")", | ||
| }) | ||
| end | ||
| else | ||
| new_conf_version = conf_version + 1 | ||
| end | ||
|
|
||
|
|
||
| apisix_yaml[conf_version_key] = new_conf_version | ||
| if new_conf_version == conf_version then | ||
| apisix_yaml[key] = config and config[key] | ||
| elseif items and #items > 0 then | ||
| apisix_yaml[key] = table_new(#items, 0) | ||
| local item_schema = resource.schema | ||
| local item_checker = resource.checker | ||
| local id_set = {} | ||
|
|
||
| for index, item in ipairs(items) do | ||
| local item_temp = tbl_deepcopy(item) | ||
| local valid, err = check_conf(item_checker, item_schema, item_temp, key) | ||
| if not valid then | ||
| local err_prefix = "invalid " .. key .. " at index " .. (index - 1) .. ", err: " | ||
| local err_msg = type(err) == "table" and err.error_msg or err | ||
| core.response.exit(400, { error_msg = err_prefix .. err_msg }) | ||
| end | ||
| for _, item in ipairs(items) do | ||
| -- prevent updating resource with the same ID | ||
| -- (e.g., service ID or other resource IDs) in a single request | ||
| local duplicated, err = check_duplicate(item, key, id_set) | ||
| if duplicated then | ||
| core.log.error(err) | ||
| core.response.exit(400, { error_msg = err }) | ||
| return core.response.exit(400, { error_msg = err }) | ||
| end | ||
|
|
||
| table_insert(apisix_yaml[key], item) | ||
|
|
@@ -280,17 +392,16 @@ local function update(ctx) | |
| return core.response.exit(202) | ||
| end | ||
|
|
||
|
|
||
| local function get(ctx) | ||
| local accept = core.request.header(nil, "accept") or "application/json" | ||
| local want_yaml_resp = core.string.has_prefix(accept, "application/yaml") | ||
|
|
||
| local config, err = get_config() | ||
| if not config then | ||
| if err ~= NOT_FOUND_ERR then | ||
| core.log.error("failed to get config from shared dict: ", err) | ||
| core.log.error("failed to get config from shared_dict: ", err) | ||
| return core.response.exit(500, { | ||
| error_msg = "failed to get config from shared dict: " .. err | ||
| error_msg = "failed to get config from shared_dict: " .. err | ||
| }) | ||
| end | ||
| config = {} | ||
|
|
@@ -330,14 +441,13 @@ local function get(ctx) | |
| return core.response.exit(200, resp) | ||
| end | ||
|
|
||
|
|
||
| local function head(ctx) | ||
| local config, err = get_config() | ||
| if not config then | ||
| if err ~= NOT_FOUND_ERR then | ||
| core.log.error("failed to get config from shared dict: ", err) | ||
| core.log.error("failed to get config from shared_dict: ", err) | ||
| return core.response.exit(500, { | ||
| error_msg = "failed to get config from shared dict: " .. err | ||
| error_msg = "failed to get config from shared_dict: " .. err | ||
| }) | ||
| end | ||
| end | ||
|
|
@@ -347,20 +457,24 @@ local function head(ctx) | |
| return core.response.exit(200) | ||
| end | ||
|
|
||
|
|
||
| function _M.run() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another Lua style: function _M.run()
local ctx = ngx.ctx.api_ctx
local method = str_lower(get_method())
if method == "put" then
return update(ctx)
end
if method == "post" then
local path = ctx.var.uri
if path == "/apisix/admin/configs/validate" then
return validate(ctx)
else
return core.response.exit(404, {error_msg = "Not found"})
end
end
if method == "head" then
return head(ctx)
end
-- default is get
return get(ctx)
end |
||
| local ctx = ngx.ctx.api_ctx | ||
| local method = str_lower(get_method()) | ||
| if method == "put" then | ||
| return update(ctx) | ||
| elseif method == "post" then | ||
| local path = ctx.var.uri | ||
| if path == "/apisix/admin/configs/validate" then | ||
| return validate(ctx) | ||
| else | ||
| return core.response.exit(404, {error_msg = "Not found"}) | ||
| end | ||
| elseif method == "head" then | ||
| return head(ctx) | ||
| else | ||
| return get(ctx) | ||
| end | ||
| end | ||
|
|
||
|
|
||
| local patch_schema | ||
| do | ||
| local resource_schema = { | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic of
check_deuplicateexists twice, the first time invalidate_configuration, and the second time is redundant.