622 lines
16 KiB
Lua
622 lines
16 KiB
Lua
local lmdb = require("resty.lmdb")
|
|
local txn = require("resty.lmdb.transaction")
|
|
local constants = require("kong.constants")
|
|
local workspaces = require("kong.workspaces")
|
|
local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy
|
|
local declarative_config = require("kong.db.schema.others.declarative_config")
|
|
local concurrency = require("kong.concurrency")
|
|
|
|
|
|
local yield = require("kong.tools.yield").yield
|
|
local marshall = require("kong.db.declarative.marshaller").marshall
|
|
local schema_topological_sort = require("kong.db.schema.topological_sort")
|
|
local nkeys = require("table.nkeys")
|
|
local sha256_hex = require("kong.tools.sha256").sha256_hex
|
|
local pk_string = declarative_config.pk_string
|
|
local EMPTY = require("kong.tools.table").EMPTY
|
|
|
|
local assert = assert
|
|
local type = type
|
|
local pairs = pairs
|
|
local insert = table.insert
|
|
local string_format = string.format
|
|
local null = ngx.null
|
|
local get_phase = ngx.get_phase
|
|
local get_workspace_id = workspaces.get_workspace_id
|
|
|
|
|
|
local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY
|
|
local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH
|
|
local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY
|
|
|
|
|
|
local GLOBAL_WORKSPACE_TAG = "*"
|
|
local UNINIT_WORKSPACE_ID = "00000000-0000-0000-0000-000000000000"
|
|
|
|
|
|
local function get_default_workspace()
|
|
-- in init phase we can not access lmdb
|
|
if kong.default_workspace == UNINIT_WORKSPACE_ID and
|
|
get_phase() ~= "init"
|
|
then
|
|
local res = kong.db.workspaces:select_by_name("default")
|
|
kong.default_workspace = assert(res and res.id)
|
|
end
|
|
|
|
return kong.default_workspace
|
|
end
|
|
|
|
|
|
-- Generates the appropriate workspace ID for current operating context
|
|
-- depends on schema settings
|
|
--
|
|
-- Non-workspaceable entities are always placed under the "default"
|
|
-- workspace
|
|
--
|
|
-- If the query explicitly set options.workspace == null, then all
|
|
-- workspaces shall be used
|
|
--
|
|
-- If the query explicitly set options.workspace == "some UUID", then
|
|
-- it will be returned
|
|
--
|
|
-- Otherwise, the current workspace ID will be returned
|
|
local function workspace_id(schema, options)
|
|
if not schema.workspaceable then
|
|
return get_default_workspace()
|
|
end
|
|
|
|
-- options.workspace does not exist
|
|
if not options or not options.workspace or options.workspace == "" then
|
|
return get_workspace_id()
|
|
end
|
|
|
|
-- global query, like routes:each(page_size, GLOBAL_QUERY_OPTS)
|
|
if options.workspace == null then
|
|
return GLOBAL_WORKSPACE_TAG
|
|
end
|
|
|
|
-- options.workspace is a UUID
|
|
return options.workspace
|
|
end
|
|
|
|
|
|
local function find_or_create_current_workspace(name)
|
|
name = name or "default"
|
|
|
|
local db_workspaces = kong.db.workspaces
|
|
local workspace, err, err_t = db_workspaces:select_by_name(name)
|
|
if err then
|
|
return nil, err, err_t
|
|
end
|
|
|
|
if not workspace then
|
|
workspace, err, err_t = db_workspaces:upsert_by_name(name, {
|
|
name = name,
|
|
no_broadcast_crud_event = true,
|
|
})
|
|
if err then
|
|
return nil, err, err_t
|
|
end
|
|
end
|
|
|
|
workspaces.set_workspace(assert(workspace))
|
|
return true
|
|
end
|
|
|
|
|
|
local function load_into_db(entities, meta)
|
|
assert(type(entities) == "table")
|
|
|
|
local db = kong.db
|
|
|
|
local schemas = {}
|
|
for entity_name in pairs(entities) do
|
|
local entity = db[entity_name]
|
|
if entity then
|
|
insert(schemas, entity.schema)
|
|
|
|
else
|
|
return nil, "unknown entity: " .. entity_name
|
|
end
|
|
end
|
|
|
|
local sorted_schemas, err = schema_topological_sort(schemas)
|
|
if not sorted_schemas then
|
|
return nil, err
|
|
end
|
|
|
|
local _, err, err_t = find_or_create_current_workspace("default")
|
|
if err then
|
|
return nil, err, err_t
|
|
end
|
|
|
|
local options = {
|
|
transform = meta._transform,
|
|
}
|
|
|
|
for i = 1, #sorted_schemas do
|
|
local schema = sorted_schemas[i]
|
|
local schema_name = schema.name
|
|
|
|
local primary_key, ok, err, err_t
|
|
for _, entity in pairs(entities[schema_name]) do
|
|
entity = cycle_aware_deep_copy(entity)
|
|
entity._tags = nil
|
|
entity.ws_id = nil
|
|
|
|
primary_key = schema:extract_pk_values(entity)
|
|
|
|
ok, err, err_t = db[schema_name]:upsert(primary_key, entity, options)
|
|
if not ok then
|
|
return nil, err, err_t
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
--- Remove all nulls from declarative config.
|
|
-- Declarative config is a huge table. Use iteration
|
|
-- instead of recursion to improve performance.
|
|
local function remove_nulls(tbl)
|
|
local stk = { tbl }
|
|
local n = #stk
|
|
|
|
local cur
|
|
while n > 0 do
|
|
cur = stk[n]
|
|
|
|
stk[n] = nil
|
|
n = n - 1
|
|
|
|
if type(cur) == "table" then
|
|
for k, v in pairs(cur) do
|
|
if v == null then
|
|
cur[k] = nil
|
|
|
|
elseif type(v) == "table" then
|
|
n = n + 1
|
|
stk[n] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return tbl
|
|
end
|
|
|
|
|
|
--- Restore all nulls for declarative config.
|
|
-- Declarative config is a huge table. Use iteration
|
|
-- instead of recursion to improve performance.
|
|
local function restore_nulls(original_tbl, transformed_tbl)
|
|
local o_stk = { original_tbl }
|
|
local o_n = #o_stk
|
|
|
|
local t_stk = { transformed_tbl }
|
|
local t_n = #t_stk
|
|
|
|
local o_cur, t_cur
|
|
while o_n > 0 and o_n == t_n do
|
|
o_cur = o_stk[o_n]
|
|
o_stk[o_n] = nil
|
|
o_n = o_n - 1
|
|
|
|
t_cur = t_stk[t_n]
|
|
t_stk[t_n] = nil
|
|
t_n = t_n - 1
|
|
|
|
for k, v in pairs(o_cur) do
|
|
if v == null and
|
|
t_cur[k] == nil
|
|
then
|
|
t_cur[k] = null
|
|
|
|
elseif type(v) == "table" and
|
|
type(t_cur[k]) == "table"
|
|
then
|
|
o_n = o_n + 1
|
|
o_stk[o_n] = v
|
|
|
|
t_n = t_n + 1
|
|
t_stk[t_n] = t_cur[k]
|
|
end
|
|
end
|
|
end
|
|
|
|
return transformed_tbl
|
|
end
|
|
|
|
|
|
local function get_current_hash()
|
|
return lmdb.get(DECLARATIVE_HASH_KEY)
|
|
end
|
|
|
|
|
|
local function find_ws(entities, name)
|
|
for _, v in pairs(entities.workspaces or EMPTY) do
|
|
if v.name == name then
|
|
return v.id
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- unique key
|
|
local function unique_field_key(schema_name, ws_id, field, value)
|
|
return string_format("U|%s|%s|%s|%s", schema_name, field, ws_id, sha256_hex(value))
|
|
end
|
|
|
|
|
|
-- foreign key
|
|
local function foreign_field_key_prefix(schema_name, ws_id, field, foreign_id)
|
|
if ws_id == GLOBAL_WORKSPACE_TAG then
|
|
return string_format("F|%s|%s|%s|", schema_name, field, foreign_id)
|
|
end
|
|
|
|
return string_format("F|%s|%s|%s|%s|", schema_name, field, foreign_id, ws_id)
|
|
end
|
|
|
|
|
|
local function foreign_field_key(schema_name, ws_id, field, foreign_id, pk)
|
|
assert(ws_id ~= GLOBAL_WORKSPACE_TAG)
|
|
return foreign_field_key_prefix(schema_name, ws_id, field, foreign_id) .. pk
|
|
end
|
|
|
|
-- item key
|
|
local function item_key_prefix(schema_name, ws_id)
|
|
if ws_id == GLOBAL_WORKSPACE_TAG then
|
|
return string_format("I|%s|", schema_name)
|
|
end
|
|
|
|
return string_format("I|%s|%s|", schema_name, ws_id)
|
|
end
|
|
|
|
|
|
local function item_key(schema_name, ws_id, pk_str)
|
|
assert(ws_id ~= GLOBAL_WORKSPACE_TAG)
|
|
return item_key_prefix(schema_name, ws_id) .. pk_str
|
|
end
|
|
|
|
|
|
local function config_is_empty(entities)
|
|
-- empty configuration has no entries other than workspaces
|
|
return entities.workspaces and nkeys(entities) == 1
|
|
end
|
|
|
|
|
|
-- common implementation for
|
|
-- insert_entity_for_txn() and delete_entity_for_txn()
|
|
local function _set_entity_for_txn(t, entity_name, item, options, is_delete)
|
|
local dao = kong.db[entity_name]
|
|
local schema = dao.schema
|
|
local pk = pk_string(schema, item)
|
|
|
|
-- If the item belongs to a specific workspace,
|
|
-- use it directly without using the default one.
|
|
local ws_id = item.ws_id or workspace_id(schema, options)
|
|
|
|
local itm_key = item_key(entity_name, ws_id, pk)
|
|
|
|
-- if we are deleting, item_value and idx_value should be nil
|
|
local itm_value, idx_value
|
|
|
|
-- if we are inserting or updating
|
|
-- itm_value is serialized entity
|
|
-- idx_value is the lmdb item_key
|
|
if not is_delete then
|
|
local err
|
|
|
|
-- serialize item with possible nulls
|
|
itm_value, err = marshall(item)
|
|
if not itm_value then
|
|
return nil, err
|
|
end
|
|
|
|
idx_value = itm_key
|
|
end
|
|
|
|
-- store serialized entity into lmdb
|
|
t:set(itm_key, itm_value)
|
|
|
|
-- select_by_cache_key
|
|
if schema.cache_key then
|
|
local cache_key = dao:cache_key(item)
|
|
-- The second parameter (ws_id) is a placeholder here, because the cache_key
|
|
-- is already unique globally.
|
|
local key = unique_field_key(entity_name, get_default_workspace(),
|
|
"cache_key", cache_key)
|
|
-- store item_key or nil into lmdb
|
|
t:set(key, idx_value)
|
|
end
|
|
|
|
for fname, fdata in schema:each_field() do
|
|
local is_foreign = fdata.type == "foreign"
|
|
local fdata_reference = fdata.reference
|
|
local value = item[fname]
|
|
-- avoid overriding the outer ws_id
|
|
local field_ws_id = fdata.unique_across_ws and kong.default_workspace or ws_id
|
|
|
|
-- value may be null, we should skip it
|
|
if not value or value == null then
|
|
goto continue
|
|
end
|
|
|
|
-- value should be a string or table
|
|
|
|
local value_str
|
|
|
|
if fdata.unique then
|
|
-- unique and not a foreign key, or is a foreign key, but non-composite
|
|
-- see: validate_foreign_key_is_single_primary_key, composite foreign
|
|
-- key is currently unsupported by the DAO
|
|
if type(value) == "table" then
|
|
assert(is_foreign)
|
|
value_str = pk_string(kong.db[fdata_reference].schema, value)
|
|
end
|
|
|
|
local key = unique_field_key(entity_name, field_ws_id, fname, value_str or value)
|
|
-- store item_key or nil into lmdb
|
|
t:set(key, idx_value)
|
|
end
|
|
|
|
if is_foreign then
|
|
-- is foreign, generate page_for_foreign_field indexes
|
|
assert(type(value) == "table")
|
|
|
|
value_str = pk_string(kong.db[fdata_reference].schema, value)
|
|
|
|
local key = foreign_field_key(entity_name, field_ws_id, fname, value_str, pk)
|
|
-- store item_key or nil into lmdb
|
|
t:set(key, idx_value)
|
|
end
|
|
|
|
::continue::
|
|
end -- for fname, fdata in schema:each_field()
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- Serialize and set keys for a single validated entity into
|
|
-- the provided LMDB txn object, this operation is only safe
|
|
-- is the entity does not already exist inside the LMDB database
|
|
--
|
|
-- The actual item key is: I|<entity_name>|<ws_id>|<pk_string>
|
|
--
|
|
-- This function sets the following key-value pairs:
|
|
--
|
|
-- key: I|<entity_name>|<ws_id>|<pk_string>
|
|
-- value: serialized item
|
|
--
|
|
-- key: U|<entity_name>|<unique_field_name>|<ws_id>|sha256(field_value)
|
|
-- value: actual item key
|
|
--
|
|
-- key: F|<entity_name>|<foreign_field_name>|<foreign_key>|<ws_id>|<pk_string>
|
|
-- value: actual item key
|
|
--
|
|
-- The format of the key string follows the sequence of the construction order:
|
|
-- `item type > entity name > specific item info > workspace id > item uuid`
|
|
-- This order makes it easier to query all entities using API lmdb_prefix.page().
|
|
--
|
|
-- DO NOT touch `item`, or else the entity will be changed
|
|
local function insert_entity_for_txn(t, entity_name, item, options)
|
|
return _set_entity_for_txn(t, entity_name, item, options, false)
|
|
end
|
|
|
|
|
|
-- Serialize and remove keys for a single validated entity into
|
|
-- the provided LMDB txn object, this operation is safe whether the provided
|
|
-- entity exists inside LMDB or not, but the provided entity must contains the
|
|
-- correct field value so indexes can be deleted correctly
|
|
local function delete_entity_for_txn(t, entity_name, item, options)
|
|
return _set_entity_for_txn(t, entity_name, item, options, true)
|
|
end
|
|
|
|
|
|
-- entities format:
|
|
-- {
|
|
-- services: {
|
|
-- ["<uuid>"] = { ... },
|
|
-- ...
|
|
-- },
|
|
-- ...
|
|
-- }
|
|
-- meta format:
|
|
-- {
|
|
-- _format_version: "3.0",
|
|
-- _transform: true,
|
|
-- }
|
|
local function load_into_cache(entities, meta, hash)
|
|
local default_workspace_id = assert(find_ws(entities, "default"))
|
|
local should_transform = meta._transform == nil and true or meta._transform
|
|
|
|
assert(type(default_workspace_id) == "string")
|
|
|
|
-- set it for insert_entity_for_txn()
|
|
kong.default_workspace = default_workspace_id
|
|
|
|
if not hash or hash == "" or config_is_empty(entities) then
|
|
hash = DECLARATIVE_EMPTY_CONFIG_HASH
|
|
end
|
|
|
|
local db = kong.db
|
|
|
|
local t = txn.begin(512)
|
|
t:db_drop(false)
|
|
|
|
local phase = get_phase()
|
|
|
|
for entity_name, items in pairs(entities) do
|
|
yield(true, phase)
|
|
|
|
local dao = db[entity_name]
|
|
if not dao then
|
|
return nil, "unknown entity: " .. entity_name
|
|
end
|
|
local schema = dao.schema
|
|
|
|
for _, item in pairs(items) do
|
|
if not schema.workspaceable or item.ws_id == null or item.ws_id == nil then
|
|
item.ws_id = default_workspace_id
|
|
end
|
|
|
|
assert(type(item.ws_id) == "string")
|
|
|
|
if should_transform and schema:has_transformations(item) then
|
|
local transformed_item = cycle_aware_deep_copy(item)
|
|
remove_nulls(transformed_item)
|
|
|
|
local err
|
|
transformed_item, err = schema:transform(transformed_item)
|
|
if not transformed_item then
|
|
return nil, err
|
|
end
|
|
|
|
item = restore_nulls(item, transformed_item)
|
|
if not item then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
-- nil means no extra options
|
|
local res, err = insert_entity_for_txn(t, entity_name, item, nil)
|
|
if not res then
|
|
return nil, err
|
|
end
|
|
end -- for for _, item
|
|
end -- for entity_name, items
|
|
|
|
t:set(DECLARATIVE_HASH_KEY, hash)
|
|
t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, default_workspace_id)
|
|
|
|
local ok, err = t:commit()
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
kong.core_cache:purge()
|
|
kong.cache:purge()
|
|
|
|
return true, nil, default_workspace_id
|
|
end
|
|
|
|
|
|
local load_into_cache_with_events
|
|
do
|
|
local events = require("kong.runloop.events")
|
|
|
|
local md5 = ngx.md5
|
|
local min = math.min
|
|
|
|
local exiting = ngx.worker.exiting
|
|
|
|
local function load_into_cache_with_events_no_lock(entities, meta, hash, hashes)
|
|
if exiting() then
|
|
return nil, "exiting"
|
|
end
|
|
|
|
local ok, err, default_ws = load_into_cache(entities, meta, hash)
|
|
if not ok then
|
|
if err:find("MDB_MAP_FULL", nil, true) then
|
|
return nil, "map full"
|
|
end
|
|
|
|
return nil, err
|
|
end
|
|
|
|
local router_hash
|
|
local plugins_hash
|
|
local balancer_hash
|
|
|
|
if hashes then
|
|
if hashes.routes ~= DECLARATIVE_EMPTY_CONFIG_HASH then
|
|
router_hash = md5(hashes.services .. hashes.routes)
|
|
else
|
|
router_hash = DECLARATIVE_EMPTY_CONFIG_HASH
|
|
end
|
|
|
|
plugins_hash = hashes.plugins
|
|
|
|
local upstreams_hash = hashes.upstreams
|
|
local targets_hash = hashes.targets
|
|
if upstreams_hash ~= DECLARATIVE_EMPTY_CONFIG_HASH or
|
|
targets_hash ~= DECLARATIVE_EMPTY_CONFIG_HASH
|
|
then
|
|
balancer_hash = md5(upstreams_hash .. targets_hash)
|
|
else
|
|
balancer_hash = DECLARATIVE_EMPTY_CONFIG_HASH
|
|
end
|
|
end
|
|
|
|
local reconfigure_data = {
|
|
default_ws,
|
|
router_hash,
|
|
plugins_hash,
|
|
balancer_hash,
|
|
}
|
|
|
|
ok, err = events.declarative_reconfigure_notify(reconfigure_data)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
if exiting() then
|
|
return nil, "exiting"
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- If it takes more than 60s it is very likely to be an internal error.
|
|
-- However it will be reported as: "failed to broadcast reconfigure event: recursive".
|
|
-- Let's paste the error message here in case someday we try to search it.
|
|
-- Should we handle this case specially?
|
|
local DECLARATIVE_LOCK_TTL = 60
|
|
local DECLARATIVE_RETRY_TTL_MAX = 10
|
|
local DECLARATIVE_LOCK_KEY = "declarative:lock"
|
|
|
|
load_into_cache_with_events = function(entities, meta, hash, hashes)
|
|
local ok, err, ttl = concurrency.with_worker_mutex({
|
|
name = DECLARATIVE_LOCK_KEY,
|
|
timeout = 0,
|
|
exptime = DECLARATIVE_LOCK_TTL,
|
|
}, function()
|
|
return load_into_cache_with_events_no_lock(entities, meta, hash, hashes)
|
|
end)
|
|
|
|
if not ok then
|
|
if err == "timeout" then
|
|
ttl = ttl or DECLARATIVE_RETRY_TTL_MAX
|
|
local retry_after = min(ttl, DECLARATIVE_RETRY_TTL_MAX)
|
|
return nil, "busy", retry_after
|
|
end
|
|
|
|
return nil, err
|
|
end
|
|
|
|
return ok, err
|
|
end
|
|
end
|
|
|
|
|
|
return {
|
|
get_current_hash = get_current_hash,
|
|
unique_field_key = unique_field_key,
|
|
foreign_field_key = foreign_field_key,
|
|
foreign_field_key_prefix = foreign_field_key_prefix,
|
|
item_key = item_key,
|
|
item_key_prefix = item_key_prefix,
|
|
workspace_id = workspace_id,
|
|
|
|
load_into_db = load_into_db,
|
|
load_into_cache = load_into_cache,
|
|
load_into_cache_with_events = load_into_cache_with_events,
|
|
insert_entity_for_txn = insert_entity_for_txn,
|
|
delete_entity_for_txn = delete_entity_for_txn,
|
|
|
|
GLOBAL_WORKSPACE_TAG = GLOBAL_WORKSPACE_TAG,
|
|
}
|