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

227 lines
6.7 KiB
Lua

local cjson = require "cjson.safe"
local raw_log = require "ngx.errlog".raw_log
local worker_id = ngx.worker.id
local native_timer_at = _G.native_timer_at or ngx.timer.at
local _, ngx_pipe = pcall(require, "ngx.pipe")
local kong = kong
local ngx_INFO = ngx.INFO
local cjson_decode = cjson.decode
local SIGTERM = 15
local server_rt = {} -- store runtime of plugin server like proc
local _M = {}
--[[
Plugin info requests
Disclaimer: The best way to do it is to have "ListPlugins()" and "GetInfo(plugin)"
RPC methods; but Kong would like to have all the plugin schemas at initialization time,
before full cosocket is available. At one time, we used blocking I/O to do RPC at
non-yielding phases, but was considered dangerous. The alternative is to use
`io.popen(cmd)` to ask fot that info.
The pluginserver_XXX_query_cmd contains a string to be executed as a command line.
The output should be a JSON string that decodes as an array of objects, each
defining the name, priority, version, schema and phases of one plugin.
[{
"name": ... ,
"priority": ... ,
"version": ... ,
"schema": ... ,
"phases": [ phase_names ... ],
},
{
...
},
...
]
This array should describe all plugins currently available through this server,
no matter if actually enabled in Kong's configuration or not.
--]]
local function query_external_plugin_info(server_def)
if not server_def.query_command then
return nil, "no info query for " .. server_def.name
end
local fd, err = io.popen(server_def.query_command)
if not fd then
return nil, string.format("error loading plugins info from [%s]: %s", server_def.name, err)
end
local infos_dump = fd:read("*a")
fd:close()
local dump, err = cjson_decode(infos_dump)
if err then
return nil, "failed decoding plugin info: " .. err
end
if type(dump) ~= "table" then
return nil, string.format("not a plugin info table: \n%s\n%s", server_def.query_command, infos_dump)
end
server_def.protocol = dump.Protocol or "MsgPack:1"
local info = (dump.Plugins or dump)[1] -- XXX can a pluginserver (in the embedded plugin server world
-- have more than one plugin? only a single
-- configuration is initialized currently, so this
-- seems to be legacy code)
-- in remote times, a plugin server could serve more than one plugin
-- nowadays (2.8+), external plugins use an "embedded pluginserver" model, where
-- each plugin acts as an independent plugin server
return {
server_def = server_def,
name = info.Name,
PRIORITY = info.Priority,
VERSION = info.Version,
schema = info.Schema,
phases = info.Phases,
}
end
function _M.load_external_plugins_info(kong_conf)
local available_external_plugins = {}
kong.log.notice("[pluginserver] loading external plugins info")
for _, pluginserver in ipairs(kong_conf.pluginservers) do
local plugin_info, err = query_external_plugin_info(pluginserver)
if not plugin_info then
return nil, err
end
available_external_plugins[plugin_info.name] = plugin_info
end
kong.log.notice("[pluginserver] loaded #", #kong_conf.pluginservers, " external plugins info")
return available_external_plugins
end
--[[
Process management
Pluginservers with a corresponding `pluginserver_XXX_start_cmd` field are managed
by Kong. Stdout and stderr are joined and logged, if it dies, Kong logs the
event and respawns the server.
If the `_start_cmd` is unset (and the default doesn't exist in the filesystem)
it's assumed the process is managed externally.
--]]
local function grab_logs(proc, name)
local prefix = string.format("[%s:%d] ", name, proc:pid())
while true do
local data, err, partial = proc:stdout_read_line()
local line = data or partial
if line and line ~= "" then
raw_log(ngx_INFO, prefix .. line)
end
if not data and (err == "closed" or ngx.worker.exiting()) then
return
end
end
end
local function pluginserver_timer(premature, server_def)
if premature then
return
end
if ngx.config.subsystem ~= "http" then
return
end
local next_spawn = 0
while not ngx.worker.exiting() do
if ngx.now() < next_spawn then
ngx.sleep(next_spawn - ngx.now())
end
kong.log.notice("[pluginserver] starting pluginserver process for ", server_def.name or "")
local proc = assert(ngx_pipe.spawn("exec " .. server_def.start_command, {
merge_stderr = true,
}))
server_rt[server_def.name] = {
proc = proc,
}
next_spawn = ngx.now() + 1
proc:set_timeouts(nil, nil, nil, 0) -- block until something actually happens
kong.log.notice("[pluginserver] started, pid ", proc:pid())
while true do
grab_logs(proc, server_def.name)
local ok, reason, status = proc:wait()
-- exited with a non 0 status
if ok == false and reason == "exit" and status == 127 then
kong.log.err(string.format(
"[pluginserver] external pluginserver %q start command %q exited with \"command not found\"",
server_def.name, server_def.start_command))
break
-- waited on an exited thread
elseif ok ~= nil or reason == "exited" or ngx.worker.exiting() then
kong.log.notice("external pluginserver '", server_def.name, "' terminated: ", tostring(reason), " ", tostring(status))
break
end
-- XXX what happens if the process stops with a 0 status code?
end
end
kong.log.notice("[pluginserver] exiting: pluginserver '", server_def.name, "' not respawned.")
end
function _M.start_pluginservers()
local kong_config = kong.configuration
-- only worker 0 manages plugin server processes
if worker_id() == 0 then -- TODO move to privileged worker?
local pluginserver_timer = pluginserver_timer
for _, server_def in ipairs(kong_config.pluginservers) do
if server_def.start_command then -- if not defined, we assume it's managed externally
native_timer_at(0, pluginserver_timer, server_def)
end
end
end
return true
end
function _M.stop_pluginservers()
local kong_config = kong.configuration
-- only worker 0 manages plugin server processes
if worker_id() == 0 then -- TODO move to privileged worker?
for _, server_def in ipairs(kong_config.pluginservers) do
local server = server_rt[server_def.name]
if server and server.proc then
local ok, err = server.proc:kill(SIGTERM)
if not ok then
kong.log.error("[pluginserver] failed to stop pluginserver '", server_def.name, ": ", err)
end
kong.log.notice("[pluginserver] successfully stopped pluginserver '", server_def.name, "', pid ", server.proc:pid())
end
end
end
return true
end
return _M