Files
2026-07-13 12:32:21 +08:00

406 lines
8.9 KiB
Lua

local schema_topological_sort = require "kong.db.schema.topological_sort"
local protobuf = require "kong.tools.protobuf"
local lyaml = require "lyaml"
local setmetatable = setmetatable
local assert = assert
local type = type
local pcall = pcall
local pairs = pairs
local insert = table.insert
local io_open = io.open
local null = ngx.null
local REMOVE_FIRST_LINE_PATTERN = "^[^\n]+\n(.+)$"
local GLOBAL_QUERY_OPTS = { nulls = true, workspace = null }
local function convert_nulls(tbl, from, to)
for k,v in pairs(tbl) do
if v == from then
tbl[k] = to
elseif type(v) == "table" then
tbl[k] = convert_nulls(v, from, to)
end
end
return tbl
end
local function to_yaml_string(tbl)
convert_nulls(tbl, null, lyaml.null)
local pok, yaml, err = pcall(lyaml.dump, { tbl })
if not pok then
return nil, yaml
end
if not yaml then
return nil, err
end
-- drop the multi-document "---\n" header and "\n..." trailer
return yaml:sub(5, -5)
end
local function to_yaml_file(entities, filename)
local yaml, err = to_yaml_string(entities)
if not yaml then
return nil, err
end
local fd, err = io_open(filename, "w")
if not fd then
return nil, err
end
local ok, err = fd:write(yaml)
if not ok then
return nil, err
end
fd:close()
return true
end
local function begin_transaction(db)
if db.strategy == "postgres" then
local ok, err = db.connector:connect("read")
if not ok then
return nil, err
end
ok, err = db.connector:query("BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;", "read")
if not ok then
return nil, err
end
end
return true
end
local function end_transaction(db)
if db.strategy == "postgres" then
-- just finish up the read-only transaction,
-- either COMMIT or ROLLBACK is fine.
db.connector:query("ROLLBACK;", "read")
db.connector:setkeepalive()
end
end
local get_latest_version
do
local strategy
local function get_latest_version_real()
return strategy:get_latest_version()
end
get_latest_version = function()
-- ensure we get the initialized kong.db
strategy = require("kong.clustering.services.sync.strategies.postgres").new(kong.db)
get_latest_version = get_latest_version_real
return get_latest_version()
end
end
local function export_from_db_impl(emitter, skip_ws, skip_disabled_entities, expand_foreigns)
local schemas = {}
local db = kong.db
for _, dao in pairs(db.daos) do
if not (skip_ws and dao.schema.name == "workspaces") then
insert(schemas, dao.schema)
end
end
local sorted_schemas, err = schema_topological_sort(schemas)
if not sorted_schemas then
return nil, err
end
local ok
ok, err = begin_transaction(db)
if not ok then
return nil, err
end
local sync_version
if emitter.want_sync_version then
sync_version = get_latest_version()
end
emitter:emit_toplevel({
_format_version = "3.0",
_transform = false,
_sync_version = sync_version, -- only used by sync emitter, DP doesn't care about this
})
local disabled_services = {}
local disabled_routes = {}
for i = 1, #sorted_schemas do
local schema = sorted_schemas[i]
if schema.db_export == false then
goto continue
end
local name = schema.name
local fks = {}
for field_name, field in schema:each_field() do
if field.type == "foreign" then
insert(fks, field_name)
end
end
local page_size
if db[name].pagination then
page_size = db[name].pagination.max_page_size
end
for row, err in db[name]:each_for_export(page_size, GLOBAL_QUERY_OPTS) do
if not row then
end_transaction(db)
kong.log.err(err)
return nil, err
end
-- do not export disabled services and disabled plugins when skip_disabled_entities
-- as well do not export plugins and routes of dsiabled services
if skip_disabled_entities and name == "services" and not row.enabled then
disabled_services[row.id] = true
elseif skip_disabled_entities and name == "routes" and row.service and
disabled_services[row.service ~= null and row.service.id] then
disabled_routes[row.id] = true
elseif skip_disabled_entities and name == "plugins" and not row.enabled then
goto skip_emit
else
for j = 1, #fks do
local foreign_name = fks[j]
if type(row[foreign_name]) == "table" then
local id = row[foreign_name].id
if id ~= nil then
if disabled_services[id] or disabled_routes[id] then
goto skip_emit
end
if not expand_foreigns then
row[foreign_name] = id
end
end
end
end
emitter:emit_entity(name, row)
end
::skip_emit::
end
::continue::
end
end_transaction(db)
return emitter:done()
end
local fd_emitter = {
emit_toplevel = function(self, tbl)
self.fd:write(to_yaml_string(tbl))
end,
emit_entity = function(self, entity_name, entity_data)
local yaml = to_yaml_string({ [entity_name] = { entity_data } })
if entity_name == self.current_entity then
yaml = assert(yaml:match(REMOVE_FIRST_LINE_PATTERN))
end
self.fd:write(yaml)
self.current_entity = entity_name
end,
done = function()
return true
end,
}
function fd_emitter.new(fd)
return setmetatable({ fd = fd }, { __index = fd_emitter })
end
local function export_from_db(fd, skip_ws, skip_disabled_entities)
-- not sure if this really useful for skip_ws,
-- but I want to allow skip_disabled_entities and would rather have consistent interface
if skip_ws == nil then
skip_ws = true
end
if skip_disabled_entities == nil then
skip_disabled_entities = false
end
return export_from_db_impl(fd_emitter.new(fd), skip_ws, skip_disabled_entities)
end
local table_emitter = {
emit_toplevel = function(self, tbl)
self.out = tbl
end,
emit_entity = function(self, entity_name, entity_data)
if not self.out[entity_name] then
self.out[entity_name] = { entity_data }
else
insert(self.out[entity_name], entity_data)
end
end,
done = function(self)
return self.out
end,
}
function table_emitter.new()
return setmetatable({}, { __index = table_emitter })
end
local function export_config(skip_ws, skip_disabled_entities)
-- default skip_ws=false and skip_disabled_services=true
if skip_ws == nil then
skip_ws = false
end
if skip_disabled_entities == nil then
skip_disabled_entities = true
end
return export_from_db_impl(table_emitter.new(), skip_ws, skip_disabled_entities)
end
local function remove_nulls(tbl)
for k,v in pairs(tbl) do
if v == null then
tbl[k] = nil
elseif type(v) == "table" then
tbl[k] = remove_nulls(v)
end
end
return tbl
end
local proto_emitter = {
emit_toplevel = function(self, tbl)
self.out = {
format_version = tbl._format_version,
}
end,
emit_entity = function(self, entity_name, entity_data)
if entity_name == "plugins" then
entity_data.config = protobuf.pbwrap_struct(entity_data.config)
end
if not self.out[entity_name] then
self.out[entity_name] = { entity_data }
else
insert(self.out[entity_name], entity_data)
end
end,
done = function(self)
return remove_nulls(self.out)
end,
}
function proto_emitter.new()
return setmetatable({}, { __index = proto_emitter })
end
local function export_config_proto(skip_ws, skip_disabled_entities)
-- default skip_ws=false and skip_disabled_services=true
if skip_ws == nil then
skip_ws = false
end
if skip_disabled_entities == nil then
skip_disabled_entities = true
end
return export_from_db_impl(proto_emitter.new(), skip_ws, skip_disabled_entities, true)
end
local function sanitize_output(entities)
entities.workspaces = nil
for _, s in pairs(entities) do -- set of entities
for _, e in pairs(s) do -- individual entity
e.ws_id = nil
end
end
end
local sync_emitter = {
emit_toplevel = function(self, tbl)
self.out = {}
self.out_n = 0
self.sync_version = tbl._sync_version
end,
emit_entity = function(self, entity_name, entity_data)
self.out_n = self.out_n + 1
self.out[self.out_n] = { type = entity_name , entity = entity_data, version = self.sync_version,
ws_id = kong.default_workspace, }
end,
done = function(self)
return self.out
end,
}
function sync_emitter.new()
return setmetatable({ want_sync_version = true, }, { __index = sync_emitter })
end
local function export_config_sync()
return export_from_db_impl(sync_emitter.new(), false, false, true)
end
return {
convert_nulls = convert_nulls,
to_yaml_string = to_yaml_string,
to_yaml_file = to_yaml_file,
export_from_db = export_from_db,
export_config = export_config,
export_config_proto = export_config_proto,
export_config_sync = export_config_sync,
sanitize_output = sanitize_output,
}