Skip to content

Commit 896d3c3

Browse files
authored
feat: add validate API to standalone mode (#12718)
1 parent 7816e92 commit 896d3c3

File tree

3 files changed

+449
-55
lines changed

3 files changed

+449
-55
lines changed

apisix/admin/init.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,11 @@ local standalone_uri_route = {
468468
methods = {"GET", "PUT", "HEAD"},
469469
handler = standalone_run,
470470
},
471+
{
472+
paths = [[/apisix/admin/configs/validate]],
473+
methods = {"POST"},
474+
handler = standalone_run,
475+
},
471476
}
472477

473478

apisix/admin/standalone.lua

Lines changed: 143 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
--
21
-- Licensed to the Apache Software Foundation (ASF) under one or more
32
-- contributor license agreements. See the NOTICE file distributed with
43
-- this work for additional information regarding copyright ownership.
@@ -22,13 +21,13 @@ local str_find = string.find
2221
local str_sub = string.sub
2322
local tostring = tostring
2423
local ngx = ngx
24+
local pcall = pcall
2525
local ngx_time = ngx.time
2626
local get_method = ngx.req.get_method
2727
local shared_dict = ngx.shared["standalone-config"]
2828
local timer_every = ngx.timer.every
2929
local exiting = ngx.worker.exiting
3030
local table_insert = table.insert
31-
local table_new = require("table.new")
3231
local yaml = require("lyaml")
3332
local events = require("apisix.events")
3433
local core = require("apisix.core")
@@ -158,6 +157,114 @@ local function check_conf(checker, schema, item, typ)
158157
end
159158

160159

160+
local function validate_configuration(req_body, collect_all_errors)
161+
local is_valid = true
162+
local validation_results = {}
163+
164+
for key, conf_version_key in pairs(ALL_RESOURCE_KEYS) do
165+
local items = req_body[key]
166+
local resource = resources[key] or {}
167+
168+
-- Validate conf_version_key if present
169+
local new_conf_version = req_body[conf_version_key]
170+
if new_conf_version and type(new_conf_version) ~= "number" then
171+
if not collect_all_errors then
172+
return false, conf_version_key .. " must be a number"
173+
end
174+
is_valid = false
175+
table_insert(validation_results, {
176+
resource_type = key,
177+
error = conf_version_key .. " must be a number, got " .. type(new_conf_version)
178+
})
179+
end
180+
181+
if items and #items > 0 then
182+
local item_schema = resource.schema
183+
local item_checker = resource.checker
184+
local id_set = {}
185+
186+
for index, item in ipairs(items) do
187+
local item_temp = tbl_deepcopy(item)
188+
local valid, err = check_conf(item_checker, item_schema, item_temp, key)
189+
if not valid then
190+
local err_prefix = "invalid " .. key .. " at index " .. (index - 1) .. ", err: "
191+
local err_msg = type(err) == "table" and err.error_msg or err
192+
local error_msg = err_prefix .. err_msg
193+
194+
if not collect_all_errors then
195+
return false, error_msg
196+
end
197+
is_valid = false
198+
table_insert(validation_results, {
199+
resource_type = key,
200+
index = index - 1,
201+
error = error_msg
202+
})
203+
end
204+
205+
-- check for duplicate IDs
206+
local duplicated, dup_err = check_duplicate(item, key, id_set)
207+
if duplicated then
208+
if not collect_all_errors then
209+
return false, dup_err
210+
end
211+
is_valid = false
212+
table_insert(validation_results, {
213+
resource_type = key,
214+
index = index - 1,
215+
error = dup_err
216+
})
217+
end
218+
end
219+
end
220+
end
221+
222+
if collect_all_errors then
223+
return is_valid, validation_results
224+
end
225+
226+
return is_valid, nil
227+
end
228+
229+
local function validate(ctx)
230+
local content_type = core.request.header(nil, "content-type") or "application/json"
231+
local req_body, err = core.request.get_body()
232+
if err then
233+
return core.response.exit(400, {error_msg = "invalid request body: " .. err})
234+
end
235+
236+
if not req_body or #req_body <= 0 then
237+
return core.response.exit(400, {error_msg = "invalid request body: empty request body"})
238+
end
239+
240+
local data
241+
if core.string.has_prefix(content_type, "application/yaml") then
242+
local ok, result = pcall(yaml.load, req_body, { all = false })
243+
if not ok or type(result) ~= "table" then
244+
err = "invalid yaml request body"
245+
else
246+
data = result
247+
end
248+
else
249+
data, err = core.json.decode(req_body)
250+
end
251+
252+
if err then
253+
core.log.error("invalid request body: ", req_body, " err: ", err)
254+
return core.response.exit(400, {error_msg = "invalid request body: " .. err})
255+
end
256+
257+
local valid, validation_results = validate_configuration(data, true)
258+
if not valid then
259+
return core.response.exit(400, {
260+
error_msg = "Configuration validation failed",
261+
errors = validation_results
262+
})
263+
end
264+
265+
return core.response.exit(200)
266+
end
267+
161268
local function update(ctx)
162269
-- check digest header existence
163270
local digest = core.request.header(nil, METADATA_DIGEST)
@@ -195,13 +302,11 @@ local function update(ctx)
195302
req_body = data
196303

197304
local config, err = get_config()
198-
if not config then
199-
if err ~= NOT_FOUND_ERR then
200-
core.log.error("failed to get config from shared dict: ", err)
201-
return core.response.exit(500, {
202-
error_msg = "failed to get config from shared dict: " .. err
203-
})
204-
end
305+
if err and err ~= NOT_FOUND_ERR then
306+
core.log.error("failed to get config from shared dict: ", err)
307+
return core.response.exit(500, {
308+
error_msg = "failed to get config from shared dict: " .. err
309+
})
205310
end
206311

207312
-- if the client passes in the same digest, the configuration is not updated
@@ -211,58 +316,35 @@ local function update(ctx)
211316
return core.response.exit(204)
212317
end
213318

214-
-- check input by jsonschema
319+
local valid, error_msg = validate_configuration(req_body, false)
320+
if not valid then
321+
return core.response.exit(400, { error_msg = error_msg })
322+
end
323+
324+
-- check input by jsonschema and build the final config
215325
local apisix_yaml = {}
216326

217327
for key, conf_version_key in pairs(ALL_RESOURCE_KEYS) do
218328
local conf_version = config and config[conf_version_key] or 0
219329
local items = req_body[key]
220330
local new_conf_version = req_body[conf_version_key]
221-
local resource = resources[key] or {}
222-
if not new_conf_version then
223-
new_conf_version = conf_version + 1
224-
else
225-
if type(new_conf_version) ~= "number" then
226-
return core.response.exit(400, {
227-
error_msg = conf_version_key .. " must be a number",
228-
})
229-
end
331+
332+
if new_conf_version then
230333
if new_conf_version < conf_version then
231334
return core.response.exit(400, {
232335
error_msg = conf_version_key ..
233336
" must be greater than or equal to (" .. conf_version .. ")",
234337
})
235338
end
339+
else
340+
new_conf_version = conf_version + 1
236341
end
237342

238-
239343
apisix_yaml[conf_version_key] = new_conf_version
240344
if new_conf_version == conf_version then
241345
apisix_yaml[key] = config and config[key]
242346
elseif items and #items > 0 then
243-
apisix_yaml[key] = table_new(#items, 0)
244-
local item_schema = resource.schema
245-
local item_checker = resource.checker
246-
local id_set = {}
247-
248-
for index, item in ipairs(items) do
249-
local item_temp = tbl_deepcopy(item)
250-
local valid, err = check_conf(item_checker, item_schema, item_temp, key)
251-
if not valid then
252-
local err_prefix = "invalid " .. key .. " at index " .. (index - 1) .. ", err: "
253-
local err_msg = type(err) == "table" and err.error_msg or err
254-
core.response.exit(400, { error_msg = err_prefix .. err_msg })
255-
end
256-
-- prevent updating resource with the same ID
257-
-- (e.g., service ID or other resource IDs) in a single request
258-
local duplicated, err = check_duplicate(item, key, id_set)
259-
if duplicated then
260-
core.log.error(err)
261-
core.response.exit(400, { error_msg = err })
262-
end
263-
264-
table_insert(apisix_yaml[key], item)
265-
end
347+
apisix_yaml[key] = items
266348
end
267349
end
268350

@@ -280,17 +362,16 @@ local function update(ctx)
280362
return core.response.exit(202)
281363
end
282364

283-
284365
local function get(ctx)
285366
local accept = core.request.header(nil, "accept") or "application/json"
286367
local want_yaml_resp = core.string.has_prefix(accept, "application/yaml")
287368

288369
local config, err = get_config()
289370
if not config then
290371
if err ~= NOT_FOUND_ERR then
291-
core.log.error("failed to get config from shared dict: ", err)
372+
core.log.error("failed to get config from shared_dict: ", err)
292373
return core.response.exit(500, {
293-
error_msg = "failed to get config from shared dict: " .. err
374+
error_msg = "failed to get config from shared_dict: " .. err
294375
})
295376
end
296377
config = {}
@@ -330,14 +411,13 @@ local function get(ctx)
330411
return core.response.exit(200, resp)
331412
end
332413

333-
334414
local function head(ctx)
335415
local config, err = get_config()
336416
if not config then
337417
if err ~= NOT_FOUND_ERR then
338-
core.log.error("failed to get config from shared dict: ", err)
418+
core.log.error("failed to get config from shared_dict: ", err)
339419
return core.response.exit(500, {
340-
error_msg = "failed to get config from shared dict: " .. err
420+
error_msg = "failed to get config from shared_dict: " .. err
341421
})
342422
end
343423
end
@@ -347,20 +427,28 @@ local function head(ctx)
347427
return core.response.exit(200)
348428
end
349429

350-
351430
function _M.run()
352431
local ctx = ngx.ctx.api_ctx
353432
local method = str_lower(get_method())
354433
if method == "put" then
355434
return update(ctx)
356-
elseif method == "head" then
357-
return head(ctx)
358-
else
359-
return get(ctx)
360435
end
361-
end
362436

437+
if method == "post" then
438+
local path = ctx.var.uri
439+
if path == "/apisix/admin/configs/validate" then
440+
return validate(ctx)
441+
else
442+
return core.response.exit(404, {error_msg = "Not found"})
443+
end
444+
end
445+
446+
if method == "head" then
447+
return head(ctx)
448+
end
363449

450+
return get(ctx)
451+
end
364452
local patch_schema
365453
do
366454
local resource_schema = {

0 commit comments

Comments
 (0)