675 lines
17 KiB
Lua
675 lines
17 KiB
Lua
local version = require "version"
|
|
|
|
local DAO = require "kong.db.dao"
|
|
local Entity = require "kong.db.schema.entity"
|
|
local Errors = require "kong.db.errors"
|
|
local Strategies = require "kong.db.strategies"
|
|
local MetaSchema = require "kong.db.schema.metaschema"
|
|
local constants = require "kong.constants"
|
|
local log = require "kong.cmd.utils.log"
|
|
local workspaces = require "kong.workspaces"
|
|
local knode = kong and kong.node
|
|
or require "kong.pdk.node".new()
|
|
|
|
|
|
local load_module_if_exists = require "kong.tools.module".load_module_if_exists
|
|
|
|
|
|
local fmt = string.format
|
|
local type = type
|
|
local pairs = pairs
|
|
local error = error
|
|
local ipairs = ipairs
|
|
local rawget = rawget
|
|
local setmetatable = setmetatable
|
|
|
|
|
|
local DEFAULT_LOCKS_TTL = 60 -- seconds
|
|
|
|
|
|
local DB = {}
|
|
DB.__index = function(self, k)
|
|
return DB[k] or rawget(self, "daos")[k]
|
|
end
|
|
|
|
|
|
function DB.new(kong_config, strategy)
|
|
if not kong_config then
|
|
error("missing kong_config", 2)
|
|
end
|
|
|
|
if strategy ~= nil and type(strategy) ~= "string" then
|
|
error("strategy must be a string", 2)
|
|
end
|
|
|
|
strategy = strategy or kong_config.database
|
|
|
|
-- load errors
|
|
|
|
local errors = Errors.new(strategy)
|
|
|
|
local schemas = {}
|
|
|
|
do
|
|
-- load schemas
|
|
-- core entities are for now the only source of schemas.
|
|
-- TODO: support schemas from plugins entities as well.
|
|
|
|
for _, entity_name in ipairs(constants.CORE_ENTITIES) do
|
|
local entity_schema = require("kong.db.schema.entities." .. entity_name)
|
|
|
|
-- validate core entities schema via metaschema
|
|
local ok, err_t = MetaSchema:validate(entity_schema)
|
|
if not ok then
|
|
return nil, fmt("schema of entity '%s' is invalid: %s", entity_name,
|
|
tostring(errors:schema_violation(err_t)))
|
|
end
|
|
local entity, err = Entity.new(entity_schema)
|
|
if not entity then
|
|
return nil, fmt("schema of entity '%s' is invalid: %s", entity_name,
|
|
err)
|
|
end
|
|
schemas[entity_name] = entity
|
|
|
|
-- load core entities subschemas
|
|
local subschemas
|
|
ok, subschemas = load_module_if_exists("kong.db.schema.entities." .. entity_name .. "_subschemas")
|
|
if ok then
|
|
for name, subschema in pairs(subschemas) do
|
|
local ok, err = entity:new_subschema(name, subschema)
|
|
if not ok then
|
|
return nil, ("error initializing schema for %s: %s"):format(entity_name, err)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- load strategy
|
|
|
|
local connector, strategies, err = Strategies.new(kong_config, strategy,
|
|
schemas, errors)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
local daos = {}
|
|
|
|
local self = {
|
|
daos = daos, -- each of those has the connector singleton
|
|
strategies = strategies,
|
|
connector = connector,
|
|
strategy = strategy,
|
|
errors = errors,
|
|
infos = connector:infos(),
|
|
loaded_plugins = kong_config.loaded_plugins, -- left for MigrationsState.load
|
|
}
|
|
|
|
do
|
|
-- load DAOs
|
|
|
|
for _, schema in pairs(schemas) do
|
|
local strategy = strategies[schema.name]
|
|
if not strategy then
|
|
return nil, fmt("no strategy found for schema '%s'", schema.name)
|
|
end
|
|
daos[schema.name] = DAO.new(self, schema, strategy, errors)
|
|
end
|
|
end
|
|
|
|
-- we are 200 OK
|
|
|
|
|
|
return setmetatable(self, DB)
|
|
end
|
|
|
|
|
|
local function prefix_err(self, err)
|
|
return "[" .. self.infos.strategy .. " error] " .. err
|
|
end
|
|
|
|
|
|
local function fmt_err(self, err, ...)
|
|
return prefix_err(self, fmt(err, ...))
|
|
end
|
|
|
|
|
|
function DB:init_connector()
|
|
-- I/O with the DB connector singleton
|
|
-- Implementation up to the strategy's connector. A place for:
|
|
-- - connection check
|
|
-- - prepare statements
|
|
-- - nop (default)
|
|
|
|
local ok, err = self.connector:init()
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
self.infos = self.connector:infos()
|
|
|
|
local version_constants = constants.DATABASE[self.strategy:upper()]
|
|
|
|
if version_constants then
|
|
ok, err = self:check_version_compat(version_constants.MIN,
|
|
version_constants.DEPRECATED)
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:init_worker()
|
|
-- Can be used to implement e.g. a timer jobs to
|
|
-- clean expired records from database in case the
|
|
-- database doesn't natively support TTL, such as
|
|
-- PostgreSQL
|
|
local ok, err = self.connector:init_worker(self.strategies)
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:connect()
|
|
local ok, err = self.connector:connect()
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:setkeepalive()
|
|
local ok, err = self.connector:setkeepalive()
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:close()
|
|
local ok, err = self.connector:close()
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:reset()
|
|
local ok, err = self.connector:reset()
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:truncate(table_name)
|
|
if table_name ~= nil and type(table_name) ~= "string" then
|
|
error("table_name must be a string", 2)
|
|
end
|
|
local ok, err
|
|
|
|
if table_name then
|
|
ok, err = self.connector:truncate_table(table_name)
|
|
else
|
|
ok, err = self.connector:truncate()
|
|
end
|
|
|
|
-- re-create default workspace on full or workspaces truncate
|
|
if not table_name or table_name == "workspaces" then
|
|
workspaces.upsert_default()
|
|
end
|
|
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return ok
|
|
end
|
|
|
|
|
|
function DB:set_events_handler(events)
|
|
for _, dao in pairs(self.daos) do
|
|
dao.events = events
|
|
end
|
|
end
|
|
|
|
|
|
function DB:check_version_compat(min, deprecated)
|
|
local _major_minor = self.connector.major_minor_version
|
|
|
|
if not _major_minor then
|
|
return false, "no version info on connector"
|
|
end
|
|
|
|
local major_minor = version(_major_minor)
|
|
local min_version = version(min)
|
|
local deprecated = deprecated and version(deprecated)
|
|
|
|
if major_minor < min_version then
|
|
-- Deprecated is "ok"
|
|
if deprecated and major_minor >= deprecated then
|
|
log.warn("Currently using %s %s which is considered deprecated, " ..
|
|
"please use %s or greater", self.strategy, _major_minor, min)
|
|
else
|
|
return false, fmt("Kong requires %s %s or greater (currently using %s)",
|
|
self.strategy, min, _major_minor)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
do
|
|
local concurrency = require "kong.concurrency"
|
|
|
|
|
|
|
|
local MAX_LOCK_WAIT_STEP = 2 -- seconds
|
|
|
|
|
|
function DB:cluster_mutex(key, opts, cb)
|
|
if type(key) ~= "string" then
|
|
error("key must be a string", 2)
|
|
end
|
|
|
|
local owner
|
|
local ttl
|
|
local no_wait
|
|
local no_cleanup
|
|
|
|
if opts ~= nil then
|
|
if type(opts) ~= "table" then
|
|
error("opts must be a table", 2)
|
|
end
|
|
|
|
if opts.ttl and type(opts.ttl) ~= "number" then
|
|
error("opts.ttl must be a number", 2)
|
|
end
|
|
|
|
if opts.owner and type(opts.owner) ~= "string" then
|
|
error("opts.owner must be a string", 2)
|
|
end
|
|
|
|
if opts.no_wait and type(opts.no_wait) ~= "boolean" then
|
|
error("opts.no_wait must be a boolean", 2)
|
|
end
|
|
|
|
if opts.no_cleanup and type(opts.no_cleanup) ~= "boolean" then
|
|
error("opts.no_cleanup must be a boolean", 2)
|
|
end
|
|
|
|
owner = opts.owner
|
|
ttl = opts.ttl
|
|
no_wait = opts.no_wait
|
|
no_cleanup = opts.no_cleanup
|
|
end
|
|
|
|
if type(cb) ~= "function" then
|
|
local mt = getmetatable(cb)
|
|
|
|
if not mt or type(mt.__call) ~= "function" then
|
|
error("cb must be a function", 2)
|
|
end
|
|
end
|
|
|
|
if not owner then
|
|
-- generate a random string for this worker (resty-cli or runtime nginx
|
|
-- worker)
|
|
--
|
|
-- we use the `node.get_id()` from pdk, but in the CLI context, this
|
|
-- value is ephemeral, so no assumptions should be made about the real
|
|
-- owner of a lock
|
|
local id, err = knode.get_id()
|
|
if not id then
|
|
return nil, prefix_err(self, "failed to generate lock owner: " .. err)
|
|
end
|
|
|
|
owner = id
|
|
end
|
|
|
|
if not ttl then
|
|
ttl = DEFAULT_LOCKS_TTL
|
|
end
|
|
|
|
local mutex_opts = {
|
|
name = key,
|
|
timeout = ttl,
|
|
}
|
|
return concurrency.with_worker_mutex(mutex_opts, function(elapsed)
|
|
if elapsed ~= 0 then
|
|
-- we did not acquire the worker lock, but it was released
|
|
return false
|
|
end
|
|
|
|
-- worker lock acquired, other workers are waiting on it
|
|
-- now acquire cluster lock via strategy-specific connector
|
|
|
|
local ok, err = self.connector:insert_lock(key, ttl, owner)
|
|
if err then
|
|
return nil, prefix_err(self, "failed to insert cluster lock: " .. err)
|
|
end
|
|
|
|
if not ok then
|
|
if no_wait then
|
|
-- don't wait on cluster locked
|
|
return false
|
|
end
|
|
|
|
-- waiting on cluster lock
|
|
local step = 0.1
|
|
local cluster_elapsed = 0
|
|
|
|
while cluster_elapsed < ttl do
|
|
ngx.sleep(step)
|
|
cluster_elapsed = cluster_elapsed + step
|
|
|
|
if cluster_elapsed >= ttl then
|
|
break
|
|
end
|
|
|
|
local locked, err = self.connector:read_lock(key)
|
|
if err then
|
|
return nil, prefix_err(self, "failed to read cluster lock: " .. err)
|
|
end
|
|
|
|
if not locked then
|
|
-- the cluster lock was released
|
|
return false
|
|
end
|
|
|
|
step = math.min(step * 3, MAX_LOCK_WAIT_STEP)
|
|
end
|
|
|
|
return nil, prefix_err(self, "timeout")
|
|
end
|
|
|
|
-- cluster lock acquired, run callback
|
|
|
|
local pok, perr = xpcall(cb, debug.traceback)
|
|
if not pok then
|
|
self.connector:remove_lock(key, owner)
|
|
|
|
return nil, prefix_err(self, "cluster_mutex callback threw an error: "
|
|
.. perr)
|
|
end
|
|
|
|
if not no_cleanup then
|
|
self.connector:remove_lock(key, owner)
|
|
end
|
|
|
|
return true
|
|
end)
|
|
end
|
|
end
|
|
|
|
|
|
do
|
|
-- migrations
|
|
local MigrationsState = require "kong.db.migrations.state"
|
|
|
|
|
|
local last_schema_state
|
|
|
|
|
|
function DB:schema_state()
|
|
local err
|
|
last_schema_state, err = MigrationsState.load(self)
|
|
return last_schema_state, err
|
|
end
|
|
|
|
|
|
function DB:last_schema_state()
|
|
return last_schema_state or self:schema_state()
|
|
end
|
|
|
|
|
|
function DB:schema_bootstrap()
|
|
local ok, err = self.connector:connect_migrations({ no_keyspace = true })
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
local ok, err = self.connector:schema_bootstrap(DEFAULT_LOCKS_TTL)
|
|
|
|
self.connector:close()
|
|
|
|
if not ok then
|
|
return nil, prefix_err(self, "failed to bootstrap database: " .. err)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
function DB:schema_reset()
|
|
local ok, err = self.connector:connect_migrations({ no_keyspace = true })
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
local ok, err = self.connector:schema_reset()
|
|
|
|
self.connector:close()
|
|
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
function DB:run_migrations(migrations, options)
|
|
if type(migrations) ~= "table" then
|
|
error("migrations must be a table", 2)
|
|
end
|
|
|
|
if type(options) ~= "table" then
|
|
error("options must be a table", 2)
|
|
end
|
|
|
|
local run_up = options.run_up
|
|
local run_teardown = options.run_teardown
|
|
|
|
local skip_teardown_migrations = {}
|
|
if run_teardown and options.skip_teardown_migrations then
|
|
for _, t in ipairs(options.skip_teardown_migrations) do
|
|
for _, mig in ipairs(t.migrations) do
|
|
local ok, mod = load_module_if_exists(t.namespace .. "." ..
|
|
mig.name)
|
|
if ok then
|
|
local strategy_migration = mod[self.strategy]
|
|
if strategy_migration and strategy_migration.teardown then
|
|
if not skip_teardown_migrations[t.subsystem] then
|
|
skip_teardown_migrations[t.subsystem] = {}
|
|
end
|
|
|
|
skip_teardown_migrations[t.subsystem][mig.name] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not run_up and not run_teardown then
|
|
error("options.run_up or options.run_teardown must be given", 2)
|
|
end
|
|
|
|
local ok, err = self.connector:connect_migrations()
|
|
if not ok then
|
|
return nil, prefix_err(self, err)
|
|
end
|
|
|
|
local n_migrations = 0
|
|
local n_pending = 0
|
|
|
|
for i, t in ipairs(migrations) do
|
|
log("migrating %s on %s '%s'...", t.subsystem, self.infos.db_desc,
|
|
self.infos.db_name)
|
|
|
|
for _, mig in ipairs(t.migrations) do
|
|
local ok, mod = load_module_if_exists(t.namespace .. "." ..
|
|
mig.name)
|
|
if not ok then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "failed to load migration '%s': %s",
|
|
mig.name, mod)
|
|
end
|
|
|
|
local strategy_migration = mod[self.strategy]
|
|
if not strategy_migration then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "missing %s strategy for migration '%s'",
|
|
self.strategy, mig.name)
|
|
end
|
|
|
|
log.debug("running migration: %s", mig.name)
|
|
|
|
if run_up then
|
|
-- kong migrations bootstrap
|
|
-- kong migrations up
|
|
if strategy_migration.up and strategy_migration.up ~= "" then
|
|
ok, err = self.connector:run_up_migration(mig.name,
|
|
strategy_migration.up)
|
|
if not ok then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "failed to run migration '%s' up: %s",
|
|
mig.name, err)
|
|
end
|
|
end
|
|
|
|
if strategy_migration.up_f then
|
|
local pok, perr, err = xpcall(strategy_migration.up_f, debug.traceback, self.connector)
|
|
if not pok or err then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "failed to run migration '%s' up_f: %s",
|
|
mig.name, perr or err)
|
|
end
|
|
end
|
|
|
|
local state = "executed"
|
|
if strategy_migration.teardown then
|
|
-- this migration has a teardown step for later
|
|
state = "pending"
|
|
n_pending = n_pending + 1
|
|
end
|
|
|
|
ok, err = self.connector:record_migration(t.subsystem, mig.name,
|
|
state)
|
|
if not ok then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "failed to record migration '%s': %s",
|
|
mig.name, err)
|
|
end
|
|
end
|
|
|
|
local skip_teardown = skip_teardown_migrations[t.subsystem] and
|
|
skip_teardown_migrations[t.subsystem][mig.name]
|
|
|
|
if not skip_teardown and run_teardown and strategy_migration.teardown then
|
|
-- kong migrations teardown
|
|
local f = strategy_migration.teardown
|
|
|
|
local pok, perr, err = xpcall(f, debug.traceback, self.connector)
|
|
if not pok or err then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "failed to run migration '%s' teardown: %s",
|
|
mig.name, perr or err)
|
|
end
|
|
|
|
ok, err = self.connector:record_migration(t.subsystem, mig.name,
|
|
"teardown")
|
|
if not ok then
|
|
self.connector:close()
|
|
return nil, fmt_err(self, "failed to record migration '%s': %s",
|
|
mig.name, err)
|
|
end
|
|
|
|
n_pending = math.max(n_pending - 1, 0)
|
|
end
|
|
|
|
log("%s migrated up to: %s %s", t.subsystem, mig.name,
|
|
strategy_migration.teardown and not run_teardown and "(pending)"
|
|
or "(executed)")
|
|
|
|
n_migrations = n_migrations + 1
|
|
end
|
|
end
|
|
|
|
log("%d migration%s processed", n_migrations,
|
|
n_migrations > 1 and "s" or "")
|
|
|
|
local n_executed = n_migrations - n_pending
|
|
|
|
if n_executed > 0 then
|
|
log("%d executed", n_executed)
|
|
end
|
|
|
|
if n_pending > 0 then
|
|
log("%d pending", n_pending)
|
|
end
|
|
|
|
self.connector:close()
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
--[[
|
|
function DB:load_pending_migrations(migrations)
|
|
if type(migrations) ~= "table" then
|
|
error("migrations must be a table", 2)
|
|
end
|
|
|
|
for _, t in ipairs(migrations) do
|
|
for _, mig in ipairs(t.migrations) do
|
|
local ok, mod = load_module_if_exists(t.namespace .. "." ..
|
|
mig.name)
|
|
if not ok then
|
|
return nil, fmt("failed to load migration '%s': %s", mig.name,
|
|
mod)
|
|
end
|
|
|
|
if mod.translations then
|
|
ngx.log(ngx.INFO, "loading translation functions for migration ",
|
|
"'", mig.name, "'")
|
|
|
|
for _, translation in ipairs(mod.translations) do
|
|
local dao = self.daos[translation.entity]
|
|
if not dao then
|
|
return nil, fmt("failed to load translation function for " ..
|
|
"migration '%s': no '%s' DAO exists", mig.name,
|
|
translation.entity)
|
|
end
|
|
|
|
dao:load_translations(mod.translations)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
self.connector:close()
|
|
|
|
return true
|
|
end
|
|
--]]
|
|
end
|
|
|
|
|
|
return DB
|