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

448 lines
15 KiB
Lua

local helpers = require "spec.helpers"
local DB = require "kong.db.init"
local tb_clone = require "table.clone"
local shell = require "resty.shell"
local strip = require("kong.tools.string").strip
-- Current number of migrations to execute in a new install
local nr_migrations = 1 -- 11
local lua_path = [[ KONG_LUA_PATH_OVERRIDE="./spec/fixtures/migrations/?.lua;]] ..
[[./spec/fixtures/migrations/?/init.lua;]]..
[[./spec/fixtures/custom_plugins/?.lua;]]..
[[./spec/fixtures/custom_plugins/?/init.lua;" ]]
for _, strategy in helpers.each_strategy() do
local function run_kong(cmd, env, no_lua_path_overrides)
env = env or {}
env.database = strategy
env.plugins = env.plugins or "off"
-- note: run migration command tests in a separate schema
-- so it won't affect default schema's ACL which are specially
-- set for readonly mode tests later
env.pg_schema = "kong_migrations_tests"
local lpath
if not no_lua_path_overrides then
lpath = lua_path
end
local cmdline = cmd .. " -c " .. helpers.test_conf_path
local _, code, stdout, stderr = helpers.kong_exec(cmdline, env, true, lpath)
return code, stdout, stderr
end
local function init_db()
local tmp_conf = tb_clone(helpers.test_conf)
tmp_conf.pg_schema = "kong_migrations_tests"
local db = assert(DB.new(tmp_conf, strategy))
assert(db:init_connector())
-- in spec/helpers.lua, db has already been init'ed
-- the stored connection will be reused here,
-- so we need to set schema explicitly to 'kong_migrations_tests'
assert(db:connect())
assert(db.connector:query("SET SCHEMA 'kong_migrations_tests';\n"))
finally(function()
db.connector:close()
end)
return db
end
describe("kong migrations #" .. strategy, function()
lazy_teardown(function()
run_kong("migrations reset --yes")
end)
it("rejects invalid commands", function()
local code, _, stderr = run_kong("migrations invalid")
assert.same(1, code)
assert.match("No such command for migrations: invalid", stderr, 1, true)
end)
describe("#db reset", function()
it("cannot run non-interactively without --yes", function()
local cmd = string.format(helpers.unindent [[
echo y | %s KONG_DATABASE=%s %s migrations reset --v -c %s
]], lua_path, strategy, helpers.bin_path, helpers.test_conf_path)
local ok, _, stderr, _, code = shell.run(cmd, nil, 0)
assert.falsy(ok)
assert.same(1, code)
assert.match("not a tty", stderr, 1, true)
end)
it("runs non-interactively with --yes", function()
run_kong("migrations bootstrap")
local db = init_db()
local code = run_kong("migrations reset --yes")
assert.same(0, code)
-- schema_migrations returns nil when it is reset
local migrations, err = db.connector:schema_migrations()
assert.is_nil(migrations)
assert.is_nil(err)
end)
it("runs even if database is in a bad state", function()
run_kong("migrations bootstrap")
local db = init_db()
-- valid SQL and CQL
db.connector:query("DROP TABLE locks;")
local code = run_kong("migrations reset --yes")
assert.same(0, code)
-- schema_migrations returns nil when it is reset
local migrations, err = db.connector:schema_migrations()
assert.is_nil(migrations)
assert.is_nil(err)
end)
it("does not reset twice", function()
run_kong("migrations reset --yes")
local code, stdout = run_kong("migrations reset --yes")
assert.same(1, code)
assert.match("nothing to reset", stdout, 1, true)
end)
end)
describe("bootstrap", function()
it("#db runs and bootstraps the database", function()
run_kong("migrations reset --yes")
local code, stdout = run_kong("migrations bootstrap")
assert.same(0, code)
assert.match("\nmigrating core", stdout, 1, true)
assert.match("\n" .. nr_migrations .. " migration", stdout, 1, true)
assert.match("\nDatabase is up-to-date\n", stdout, 1, true)
end)
if strategy == "off" then
it("always reports as bootstrapped", function()
local code, stdout = run_kong("migrations bootstrap")
assert.same(0, code)
assert.match("Database already bootstrapped", stdout, 1, true)
end)
end
it("does not bootstrap twice", function()
local code = run_kong("migrations bootstrap")
assert.same(0, code)
local stdout
code, stdout = run_kong("migrations bootstrap")
assert.same(0, code)
assert.match("Database already bootstrapped", stdout, 1, true)
end)
it("#db does bootstrap twice if forced", function()
local code = run_kong("migrations bootstrap")
assert.same(0, code)
local stdout
code, stdout = run_kong("migrations bootstrap --force")
assert.same(0, code)
assert.match("\nmigrating core", stdout, 1, true)
assert.match("\n" .. nr_migrations .. " migration", stdout, 1, true)
assert.match("\nDatabase is up-to-date\n", stdout, 1, true)
end)
pending("-q suppresses all output", function()
local code, stdout, stderr = run_kong("migrations bootstrap -q")
assert.same(0, code)
assert.same(0, #stdout)
assert.same(0, #stderr)
end)
it("-p accepts a prefix override", function()
local code, stdout, stderr = run_kong("migrations bootstrap -p /dev/null")
assert.equal(1, code)
assert.equal(0, #stdout)
assert.match("/dev/null is not a directory", stderr, 1, true)
end)
end)
describe("list", function()
it("#db fails if not bootstrapped", function()
local code = run_kong("migrations reset --yes")
assert.same(0, code)
local stdout
code, stdout = run_kong("migrations list")
assert.same(3, code)
assert.match("Database needs bootstrapping or is older than Kong 1.0", stdout, 1, true)
end)
it("lists migrations if bootstrapped", function()
local code = run_kong("migrations bootstrap")
assert.same(0, code)
code = run_kong("migrations up")
assert.same(0, code)
local stdout
code, stdout = run_kong("migrations list")
assert.same(0, code)
assert.match("Executed migrations:", stdout, 1, true)
if strategy ~= "off" then
-- to avoid postgresql error:
-- [PostgreSQL error] failed to retrieve PostgreSQL server_version_num: receive_message:
-- failed to get type: timeout
-- when testing on ARM64 platform which has low single-core performance
local pok, db
helpers.wait_until(function()
pok, db = pcall(init_db)
return pok
end, 10)
-- valid CQL and SQL; don't expect to go over one page in CQL here
local rows = db.connector:query([[SELECT * FROM schema_meta;]])
local n = 0
for _, row in ipairs(rows) do
n = n + #row.executed
end
assert.same(nr_migrations, n)
end
end)
it("#db lists pending migrations if any", function()
run_kong("migrations bootstrap")
local code, stdout = run_kong("migrations list", {
plugins = "with-migrations",
})
assert.same(5, code)
assert.match("Executed migrations:\n" ..
"core: 000_base\n\n" ..
"New migrations available:\n" ..
"with-migrations: 000_base_with_migrations, 001_14_to_15\n\n" ..
"Run 'kong migrations up' to proceed",
stdout, 1, true)
end)
pending("-q suppresses all output", function()
local code, stdout, stderr = run_kong("migrations list -q")
assert.same(0, code)
assert.same(0, #stdout)
assert.same(0, #stderr)
end)
end)
describe("up", function()
it("#db performs first phase of migration", function()
run_kong("migrations reset --yes")
local code = run_kong("migrations bootstrap")
assert.same(0, code)
local stdout, stderr
code, stdout, stderr = run_kong("migrations up", {
plugins = "with-migrations",
})
assert.match("2 migrations processed", stdout .. "\n" .. stderr, 1, true)
assert.match("1 executed", stdout .. "\n" .. stderr, 1, true)
assert.match("1 pending", stdout .. "\n" .. stderr, 1, true)
assert.same(0, code)
code, stdout = run_kong("migrations up")
assert.same(0, code)
assert.match("Database is already up-to-date", stdout, 1, true)
local db = init_db()
-- valid CQL and SQL; don't expect to go over one page in CQL here
local rows = db.connector:query([[SELECT * FROM schema_meta;]])
local executed = 0
local pending = 0
for _, row in ipairs(rows) do
executed = executed + #row.executed
pending = pending + (type(row.pending) == "table" and #row.pending or 0)
end
assert.same(nr_migrations + 1, executed)
assert.same(1, pending)
end)
if strategy == "off" then
it("always reports as up-to-date", function()
local code, stdout = run_kong("migrations up")
assert.same(0, code)
assert.match("Database is already up-to-date", stdout, 1, true)
end)
end
pending("-q suppresses all output", function()
local code, stdout, stderr = run_kong("migrations up -q")
assert.same(0, code)
assert.same(0, #stdout)
assert.same(0, #stderr)
end)
end)
describe("finish", function()
it("#db performs second phase of migration", function()
run_kong("migrations reset --yes")
run_kong("migrations bootstrap")
local code = run_kong("migrations up", {
plugins = "with-migrations",
})
assert.same(0, code)
local stdout, stderr
code, stdout, stderr = run_kong("migrations finish", {
plugins = "with-migrations",
})
assert.match("1 migration processed", stdout .. "\n" .. stderr, 1, true)
assert.match("1 executed", stdout .. "\n" .. stderr, 1, true)
assert.same(0, code)
code, stdout = run_kong("migrations finish")
assert.same(0, code)
assert.match("No pending migrations to finish", stdout, 1, true)
local db = init_db()
-- valid CQL and SQL; don't expect to go over one page in CQL here
local rows = db.connector:query([[SELECT * FROM schema_meta;]])
local executed = 0
local pending = 0
for _, row in ipairs(rows) do
executed = executed + #row.executed
pending = pending + (type(row.pending) == "table" and #row.pending or 0)
end
--assert.same({}, rows)
assert.same(nr_migrations + 2, executed)
assert.same(0, pending)
end)
if strategy == "off" then
it("always reports as done", function()
local code, stdout = run_kong("migrations finish")
assert.same(0, code)
assert.match("No pending migrations to finish", stdout, 1, true)
end)
end
pending("-q suppresses all output", function()
local code, stdout, stderr = run_kong("migrations finish -q")
assert.same(0, code)
assert.same(0, #stdout)
assert.same(0, #stderr)
end)
end)
describe("reentrancy " .. strategy, function()
lazy_setup(function()
run_kong("migrations reset --yes")
end)
after_each(function()
run_kong("migrations reset --yes")
end)
it("#db is reentrant with migrations up -f", function()
local _, code, stdout, stderr
code, _, stderr = run_kong("migrations reset --yes", {
plugins = "bundled"
}, true)
assert.equal(1, code)
assert.equal("", stderr)
code, _, stderr = run_kong("migrations bootstrap", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("", stderr)
code, stdout, stderr = run_kong("migrations up", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("Database is already up-to-date", strip(stdout))
assert.equal("", stderr)
code, stdout, stderr = run_kong("migrations up -f", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("", stderr)
local code2, stdout2, stderr2 = run_kong("migrations up -f", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("", stderr)
assert.equal(code, code2)
assert.equal(stdout, stdout2)
assert.equal(stderr, stderr2)
end)
it("#db is reentrant with migrations finish -f", function()
local _, code, stdout, stderr
code, _, stderr = run_kong("migrations reset --yes", {
plugins = "bundled"
}, true)
assert.equal(1, code)
assert.equal("", stderr)
code, _, stderr = run_kong("migrations bootstrap", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("", stderr)
code, stdout, stderr = run_kong("migrations up", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("Database is already up-to-date", strip(stdout))
assert.equal("", stderr)
code, stdout, stderr = run_kong("migrations finish", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("No pending migrations to finish", strip(stdout))
assert.equal("", stderr)
code, stdout, stderr = run_kong("migrations finish -f", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("", stderr)
local code2, stdout2, stderr2 = run_kong("migrations finish -f", {
plugins = "bundled"
}, true)
assert.equal(0, code)
assert.equal("", stderr)
assert.equal(code, code2)
assert.equal(stdout, stdout2)
assert.equal(stderr, stderr2)
end)
end)
end)
describe("sanity: make sure postgres server is not overloaded", function()
local do_it = strategy == "off" and pending or it
do_it("", function()
helpers.wait_until(function()
local ok, err = pcall(init_db)
if err then
print(err)
end
return ok
end, 30, 1)
end)
end)
end