------------------------------------------------------------------ -- Collection of utilities to help testing Kong features and plugins. -- -- @copyright Copyright 2016-2022 Kong Inc. All rights reserved. -- @license [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -- @module spec.helpers local lfs = require("lfs") local version = require("version") local pl_dir = require("pl.dir") local pl_path = require("pl.path") local pl_utils = require("pl.utils") local constants = require("kong.constants") local conf_loader = require("kong.conf_loader") local kong_table = require("kong.tools.table") local kill = require("kong.cmd.utils.kill") local prefix_handler = require("kong.cmd.utils.prefix_handler") local CONSTANTS = require("spec.internal.constants") local conf = require("spec.internal.conf") local shell = require("spec.internal.shell") local DB = require("spec.internal.db") local pid = require("spec.internal.pid") local dns_mock = require("spec.internal.dns") -- initialized in start_kong() local config_yml --- Return the actual Kong version the tests are running against. -- See [version.lua](https://github.com/kong/version.lua) for the format. This -- is mostly useful for testing plugins that should work with multiple Kong versions. -- @function get_version -- @return a `version` object -- @usage -- local version = require 'version' -- if helpers.get_version() < version("0.15.0") then -- -- do something -- end local function get_version() return version(select(3, assert(shell.kong_exec("version")))) end local function build_go_plugins(path) if pl_path.exists(pl_path.join(path, "go.mod")) then local ok, _, stderr = shell.run(string.format( "cd %s; go mod tidy; go mod download", path), nil, 0) assert(ok, stderr) end for _, go_source in ipairs(pl_dir.getfiles(path, "*.go")) do local ok, _, stderr = shell.run(string.format( "cd %s; go build %s", path, pl_path.basename(go_source) ), nil, 0) assert(ok, stderr) end end --- Prepares the Kong environment. -- Creates the working directory if it does not exist. -- @param prefix (optional) path to the working directory, if omitted the test -- configuration will be used -- @function prepare_prefix local function prepare_prefix(prefix) return pl_dir.makepath(prefix or conf.prefix) end --- Cleans the Kong environment. -- Deletes the working directory if it exists. -- @param prefix (optional) path to the working directory, if omitted the test -- configuration will be used -- @function clean_prefix local function clean_prefix(prefix) -- like pl_dir.rmtree, but ignore mount points local function rmtree(fullpath) if pl_path.islink(fullpath) then return false,'will not follow symlink' end for root,dirs,files in pl_dir.walk(fullpath,true) do if pl_path.islink(root) then -- sub dir is a link, remove link, do not follow local res, err = os.remove(root) if not res then return nil, err .. ": " .. root end else for i,f in ipairs(files) do f = pl_path.join(root,f) local res, err = os.remove(f) if not res then return nil,err .. ": " .. f end end local res, err = pl_path.rmdir(root) -- skip errors when trying to remove mount points if not res and shell.run("findmnt " .. root .. " 2>&1 >/dev/null", nil, 0) == 0 then return nil, err .. ": " .. root end end end return true end prefix = prefix or conf.prefix if pl_path.exists(prefix) then local _, err = rmtree(prefix) if err then error(err) end end end local function render_fixtures(conf, env, prefix, fixtures) if fixtures and (fixtures.http_mock or fixtures.stream_mock) then -- prepare the prefix so we get the full config in the -- hidden `.kong_env` file, including test specified env vars etc assert(shell.kong_exec("prepare --conf " .. conf, env)) local render_config = assert(conf_loader(prefix .. "/.kong_env", nil, { from_kong_env = true })) for _, mocktype in ipairs { "http_mock", "stream_mock" } do for filename, contents in pairs(fixtures[mocktype] or {}) do -- render the file using the full configuration contents = assert(prefix_handler.compile_conf(render_config, contents)) -- write file to prefix filename = prefix .. "/" .. filename .. "." .. mocktype assert(pl_utils.writefile(filename, contents)) end end end if fixtures and fixtures.dns_mock then -- write the mock records to the prefix assert(getmetatable(fixtures.dns_mock) == dns_mock, "expected dns_mock to be of a helpers.dns_mock class") assert(pl_utils.writefile(prefix .. "/dns_mock_records.json", tostring(fixtures.dns_mock))) -- add the mock resolver to the path to ensure the records are loaded if env.lua_package_path then env.lua_package_path = CONSTANTS.DNS_MOCK_LUA_PATH .. ";" .. env.lua_package_path else env.lua_package_path = CONSTANTS.DNS_MOCK_LUA_PATH end else -- remove any old mocks if they exist os.remove(prefix .. "/dns_mock_records.json") end return true end --- Return the actual configuration running at the given prefix. -- It may differ from the default, as it may have been modified -- by the `env` table given to start_kong. -- @function get_running_conf -- @param prefix (optional) The prefix path where the kong instance is running, -- defaults to the prefix in the default config. -- @return The conf table of the running instance, or nil + error. local function get_running_conf(prefix) local default_conf = conf_loader(nil, {prefix = prefix or conf.prefix}) return conf_loader.load_config_file(default_conf.kong_env) end --- Clears the logfile. Will overwrite the logfile with an empty file. -- @function clean_logfile -- @param logfile (optional) filename to clear, defaults to the current -- error-log file -- @return nothing -- @see line local function clean_logfile(logfile) logfile = logfile or (get_running_conf() or conf).nginx_err_logs assert(type(logfile) == "string", "'logfile' must be a string") local fh, err, errno = io.open(logfile, "w+") if fh then fh:close() return elseif errno == 2 then -- ENOENT return end error("failed to truncate logfile: " .. tostring(err)) end --- Start the Kong instance to test against. -- The fixtures passed to this function can be 3 types: -- -- * DNS mocks -- -- * Nginx server blocks to be inserted in the http module -- -- * Nginx server blocks to be inserted in the stream module -- @function start_kong -- @param env table with Kong configuration parameters (and values) -- @param tables list of database tables to truncate before starting -- @param preserve_prefix (boolean) if truthy, the prefix will not be cleaned -- before starting -- @param fixtures tables with fixtures, dns, http and stream mocks. -- @return return values from `execute` -- @usage -- -- example mocks -- -- Create a new DNS mock and add some DNS records -- local fixtures = { -- http_mock = {}, -- stream_mock = {}, -- dns_mock = helpers.dns_mock.new() -- } -- -- **DEPRECATED**: http_mock fixture is deprecated. Please use `spec.helpers.http_mock` instead. -- -- fixtures.dns_mock:A { -- name = "a.my.srv.test.com", -- address = "127.0.0.1", -- } -- -- -- The blocks below will be rendered by the Kong template renderer, like other -- -- custom Kong templates. Hence the `${{xxxx}}` values. -- -- Multiple mocks can be added each under their own filename ("my_server_block" below) -- fixtures.http_mock.my_server_block = [[ -- server { -- server_name my_server; -- listen 10001 ssl; -- -- ssl_certificate ${{SSL_CERT}}; -- ssl_certificate_key ${{SSL_CERT_KEY}}; -- ssl_protocols TLSv1.2 TLSv1.3; -- -- location ~ "/echobody" { -- content_by_lua_block { -- ngx.req.read_body() -- local echo = ngx.req.get_body_data() -- ngx.status = status -- ngx.header["Content-Length"] = #echo + 1 -- ngx.say(echo) -- } -- } -- } -- ]] -- -- fixtures.stream_mock.my_server_block = [[ -- server { -- -- insert stream server config here -- } -- ]] -- -- assert(helpers.start_kong( {database = "postgres"}, nil, nil, fixtures)) local function start_kong(env, tables, preserve_prefix, fixtures) if tables ~= nil and type(tables) ~= "table" then error("arg #2 must be a list of tables to truncate") end env = env or {} local prefix = env.prefix or conf.prefix -- go plugins are enabled -- compile fixture go plugins if any setting mentions it for _,v in pairs(env) do if type(v) == "string" and v:find(CONSTANTS.EXTERNAL_PLUGINS_PATH .. "/go") then build_go_plugins(CONSTANTS.EXTERNAL_PLUGINS_PATH .. "/go") break end end -- note: set env var "KONG_TEST_DONT_CLEAN" !! the "_TEST" will be dropped if not (preserve_prefix or os.getenv("KONG_DONT_CLEAN")) then clean_prefix(prefix) end local ok, err = prepare_prefix(prefix) if not ok then return nil, err end DB.truncate_tables(DB.db, tables) local nginx_conf = "" local nginx_conf_flags = { "test" } if env.nginx_conf then nginx_conf = " --nginx-conf " .. env.nginx_conf end if CONSTANTS.TEST_COVERAGE_MODE == "true" then -- render `coverage` blocks in the templates nginx_conf_flags[#nginx_conf_flags + 1] = 'coverage' end if next(nginx_conf_flags) then nginx_conf_flags = " --nginx-conf-flags " .. table.concat(nginx_conf_flags, ",") else nginx_conf_flags = "" end local dcbp = DB.get_dcbp() if dcbp and not env.declarative_config and not env.declarative_config_string then if not config_yml then config_yml = prefix .. "/config.yml" local cfg = dcbp.done() local declarative = require "kong.db.declarative" local ok, err = declarative.to_yaml_file(cfg, config_yml) if not ok then return nil, err end end env = kong_table.cycle_aware_deep_copy(env) env.declarative_config = config_yml end assert(render_fixtures(CONSTANTS.TEST_CONF_PATH .. nginx_conf, env, prefix, fixtures)) return shell.kong_exec("start --conf " .. CONSTANTS.TEST_CONF_PATH .. nginx_conf .. nginx_conf_flags, env) end -- Cleanup after kong test instance, should be called if start_kong was invoked with the nowait flag -- @function cleanup_kong -- @param prefix (optional) the prefix where the test instance runs, defaults to the test configuration. -- @param preserve_prefix (boolean) if truthy, the prefix will not be deleted after stopping -- @param preserve_dc ??? local function cleanup_kong(prefix, preserve_prefix, preserve_dc) -- remove socket files to ensure `pl.dir.rmtree()` ok prefix = prefix or conf.prefix local socket_path = pl_path.join(prefix, constants.SOCKET_DIRECTORY) for child in lfs.dir(socket_path) do local path = pl_path.join(socket_path, child) if lfs.attributes(path, "mode") == "socket" then os.remove(path) end end -- note: set env var "KONG_TEST_DONT_CLEAN" !! the "_TEST" will be dropped if not (preserve_prefix or os.getenv("KONG_DONT_CLEAN")) then clean_prefix(prefix) end if not preserve_dc then config_yml = nil end ngx.ctx.workspace = nil end -- Stop the Kong test instance. -- @function stop_kong -- @param prefix (optional) the prefix where the test instance runs, defaults to the test configuration. -- @param preserve_prefix (boolean) if truthy, the prefix will not be deleted after stopping -- @param preserve_dc ??? -- @param signal (optional string) signal name to send to kong, defaults to TERM -- @param nowait (optional) if truthy, don't wait for kong to terminate. caller needs to wait and call cleanup_kong -- @return true or nil+err local function stop_kong(prefix, preserve_prefix, preserve_dc, signal, nowait) prefix = prefix or conf.prefix signal = signal or "TERM" local running_conf, err = get_running_conf(prefix) if not running_conf then return nil, err end local id, err = pid.get_pid_from_file(running_conf.nginx_pid) if not id then return nil, err end local ok, _, err = shell.run(string.format("kill -%s %d", signal, id), nil, 0) if not ok then return nil, err end if nowait then return running_conf.nginx_pid end pid.wait_pid(running_conf.nginx_pid) cleanup_kong(prefix, preserve_prefix, preserve_dc) return true end --- Restart Kong. Reusing declarative config when using `database=off`. -- @function restart_kong -- @param env see `start_kong` -- @param tables see `start_kong` -- @param fixtures see `start_kong` -- @return true or nil+err local function restart_kong(env, tables, fixtures) stop_kong(env.prefix, true, true) return start_kong(env, tables, true, fixtures) end -- Only use in CLI tests from spec/02-integration/01-cmd local function kill_all(prefix, timeout) local running_conf = get_running_conf(prefix) if not running_conf then return end -- kill kong_tests.conf service local pid_path = running_conf.nginx_pid if pl_path.exists(pid_path) then kill.kill(pid_path, "-TERM") pid.wait_pid(pid_path, timeout) end end local function signal(prefix, signal, pid_path) if not pid_path then local running_conf = get_running_conf(prefix) if not running_conf then error("no config file found at prefix: " .. prefix) end pid_path = running_conf.nginx_pid end return kill.kill(pid_path, signal) end -- send signal to all Nginx workers, not including the master local function signal_workers(prefix, signal, pid_path) if not pid_path then local running_conf = get_running_conf(prefix) if not running_conf then error("no config file found at prefix: " .. prefix) end pid_path = running_conf.nginx_pid end local cmd = string.format("pkill %s -P `cat %s`", signal, pid_path) local _, _, _, _, code = shell.run(cmd) if not pid.pid_dead(pid_path) then return false end return code end return { get_version = get_version, start_kong = start_kong, cleanup_kong = cleanup_kong, stop_kong = stop_kong, restart_kong = restart_kong, prepare_prefix = prepare_prefix, clean_prefix = clean_prefix, get_running_conf = get_running_conf, clean_logfile = clean_logfile, kill_all = kill_all, signal = signal, signal_workers = signal_workers, build_go_plugins = build_go_plugins, }