Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apisix/admin/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,11 @@ local standalone_uri_route = {
methods = {"GET", "PUT", "HEAD"},
handler = standalone_run,
},
{
paths = [[/apisix/admin/configs/validate]],
methods = {"POST"},
handler = standalone_run,
},
}


Expand Down
178 changes: 146 additions & 32 deletions apisix/admin/standalone.lua
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.
Expand All @@ -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"]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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
Expand All @@ -347,20 +457,24 @@ local function head(ctx)
return core.response.exit(200)
end


function _M.run()
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 = {
Expand Down
Loading
Loading