Skip to content

feat(standalone): support revision in API-driven standalone mode like etcd #12214

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

Merged
merged 11 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
119 changes: 80 additions & 39 deletions apisix/admin/standalone.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
local type = type
local pairs = pairs
local ipairs = ipairs
local tonumber = tonumber
local tostring = tostring
local str_lower = string.lower
local ngx = ngx
local get_method = ngx.req.get_method
Expand All @@ -30,25 +28,42 @@ local events = require("apisix.events")
local core = require("apisix.core")
local config_yaml = require("apisix.core.config_yaml")
local check_schema = require("apisix.core.schema").check
local tbl_deepcopy = require("apisix.core.table").deepcopy

local EVENT_UPDATE = "standalone-api-configuration-update"

local _M = {}


local function update_and_broadcast_config(apisix_yaml, conf_version)
local config = core.json.encode({
conf = apisix_yaml,
conf_version = conf_version,
})
local function get_config()
local config = shared_dict:get("config")
if not config then
return nil, "not found"
end

local err
config, err = core.json.decode(config)
if not config then
return nil, "failed to decode json: " .. err
end
return config
end


local function update_and_broadcast_config(apisix_yaml)
local raw, err = core.json.encode(apisix_yaml)
if not raw then
core.log.error("failed to encode json: ", err)
return nil, "failed to encode json: " .. err
end

if shared_dict then
-- the worker that handles Admin API calls is responsible for writing the shared dict
local ok, err = shared_dict:set("config", config)
local ok, err = shared_dict:set("config", raw)
if not ok then
return nil, "failed to save config to shared dict: " .. err
end
core.log.info("standalone config updated: ", config)
core.log.info("standalone config updated: ", raw)
else
core.log.crit(config_yaml.ERR_NO_SHARED_DICT)
end
Expand All @@ -59,26 +74,6 @@ end
local function update(ctx)
local content_type = core.request.header(nil, "content-type") or "application/json"

local conf_version
if ctx.var.arg_conf_version then
conf_version = tonumber(ctx.var.arg_conf_version)
if not conf_version then
return core.response.exit(400, {error_msg = "invalid conf_version: "
.. ctx.var.arg_conf_version
.. ", should be a integer" })
end
else
conf_version = ngx.time()
end
-- check if conf_version greater than the current version
local _, ver = config_yaml._get_config()
if conf_version <= ver then
return core.response.exit(400, {error_msg = "invalid conf_version: conf_version ("
.. conf_version
.. ") should be greater than the current version ("
.. ver .. ")"})
end

-- read the request body
local req_body, err = core.request.get_body()
if err then
Expand All @@ -105,28 +100,63 @@ local function update(ctx)
end
req_body = data

local config, err = get_config()
if not config then
if err ~= "not found" then
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
})
end
end

-- check input by jsonschema
local apisix_yaml = {}
local created_objs = config_yaml.fetch_all_created_obj()

for key, obj in pairs(created_objs) do
if req_body[key] and #req_body[key] > 0 then
apisix_yaml[key] = table_new(1, 0)
local conf_version_key = obj.conf_version_key
local conf_version = config and config[conf_version_key] or obj.conf_version
local items = req_body[key]
local new_conf_version = req_body[conf_version_key]
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 < conf_version then
return core.response.exit(400, {
error_msg = conf_version_key ..
" must be greater than or equal to (" .. conf_version .. ")",
})
end
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 = obj.item_schema
local item_checker = obj.checker

for index, item in ipairs(req_body[key]) do
for index, item in ipairs(items) do
local item_temp = tbl_deepcopy(item)
local valid, err
-- need to recover to 0-based subscript
local err_prefix = "invalid " .. key .. " at index " .. (index - 1) .. ", err: "
if item_schema then
valid, err = check_schema(obj.item_schema, item)
valid, err = check_schema(obj.item_schema, item_temp)
if not valid then
core.log.error(err_prefix, err)
core.response.exit(400, {error_msg = err_prefix .. err})
end
end
if item_checker then
valid, err = item_checker(item)
valid, err = item_checker(item_temp)
if not valid then
core.log.error(err_prefix, err)
core.response.exit(400, {error_msg = err_prefix .. err})
Expand All @@ -137,12 +167,11 @@ local function update(ctx)
end
end

local ok, err = update_and_broadcast_config(apisix_yaml, conf_version)
local ok, err = update_and_broadcast_config(apisix_yaml)
if not ok then
core.response.exit(500, err)
end

core.response.set_header("X-APISIX-Conf-Version", tostring(conf_version))
return core.response.exit(202)
end

Expand All @@ -151,9 +180,21 @@ 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 _, ver, config = config_yaml._get_config()
local config, err = get_config()
if not config then
if err ~= "not found" then
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
})
end
config = {}
local created_objs = config_yaml.fetch_all_created_obj()
for _, obj in pairs(created_objs) do
config[obj.conf_version_key] = obj.conf_version
end
end

core.response.set_header("X-APISIX-Conf-Version", tostring(ver))
local resp, err
if want_yaml_resp then
core.response.set_header("Content-Type", "application/yaml")
Expand Down Expand Up @@ -207,7 +248,7 @@ function _M.init_worker()
core.log.error("failed to decode json: ", err)
return
end
config_yaml._update_config(config.conf, config.conf_version)
config_yaml._update_config(config)
end
events:register(update_config, EVENT_UPDATE, EVENT_UPDATE)
end
Expand Down
Loading
Loading