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
2221local str_sub = string.sub
2322local tostring = tostring
2423local ngx = ngx
24+ local pcall = pcall
2525local ngx_time = ngx .time
2626local get_method = ngx .req .get_method
2727local shared_dict = ngx .shared [" standalone-config" ]
2828local timer_every = ngx .timer .every
2929local exiting = ngx .worker .exiting
3030local table_insert = table.insert
31- local table_new = require (" table.new" )
3231local yaml = require (" lyaml" )
3332local events = require (" apisix.events" )
3433local core = require (" apisix.core" )
@@ -158,6 +157,114 @@ local function check_conf(checker, schema, item, typ)
158157end
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+
161268local 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 )
281363end
282364
283-
284365local 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 )
331412end
332413
333-
334414local 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 )
348428end
349429
350-
351430function _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
364452local patch_schema
365453do
366454 local resource_schema = {
0 commit comments