Files
kong--kong/kong/runloop/plugins_iterator.lua
T
2026-07-13 12:32:21 +08:00

647 lines
18 KiB
Lua

local workspaces = require "kong.workspaces"
local constants = require "kong.constants"
local tablepool = require "tablepool"
local req_dyn_hook = require "kong.dynamic_hook"
local wasm = require "kong.runloop.wasm"
local kong = kong
local error = error
local assert = assert
local var = ngx.var
local null = ngx.null
local pcall = pcall
local subsystem = ngx.config.subsystem
local pairs = pairs
local ipairs = ipairs
local format = string.format
local fetch_table = tablepool.fetch
local release_table = tablepool.release
local uuid = require("kong.tools.uuid").uuid
local get_updated_monotonic_ms = require("kong.tools.time").get_updated_monotonic_ms
local req_dyn_hook_disable_by_default = req_dyn_hook.disable_by_default
local TTL_ZERO = { ttl = 0 }
local GLOBAL_QUERY_OPTS = { workspace = null, show_ws_id = true }
local NON_COLLECTING_PHASES, DOWNSTREAM_PHASES, DOWNSTREAM_PHASES_COUNT, COLLECTING_PHASE, CONFIGURE_PHASE
do
if subsystem == "stream" then
NON_COLLECTING_PHASES = {
"certificate",
"log",
}
DOWNSTREAM_PHASES = {
"log",
}
COLLECTING_PHASE = "preread"
else
NON_COLLECTING_PHASES = {
"certificate",
"rewrite",
"response",
"header_filter",
"body_filter",
"log",
}
DOWNSTREAM_PHASES = {
"response",
"header_filter",
"body_filter",
"log",
}
COLLECTING_PHASE = "access"
end
DOWNSTREAM_PHASES_COUNT = #DOWNSTREAM_PHASES
CONFIGURE_PHASE = "configure"
end
local PLUGINS_NS = "plugins." .. subsystem
local ENABLED_PLUGINS
local LOADED_PLUGINS
local CONFIGURABLE_PLUGINS
local PluginsIterator = {}
---
-- Build a compound key by string formatting route_id, service_id, and consumer_id with colons as separators.
--
-- @function build_compound_key
-- @tparam string|nil route_id The route identifier. If `nil`, an empty string is used.
-- @tparam string|nil service_id The service identifier. If `nil`, an empty string is used.
-- @tparam string|nil consumer_id The consumer identifier. If `nil`, an empty string is used.
-- @treturn string The compound key, in the format `route_id:service_id:consumer_id`.
local function build_compound_key(route_id, service_id, consumer_id)
return format("%s:%s:%s", route_id or "", service_id or "", consumer_id or "")
end
local PLUGIN_GLOBAL_KEY = build_compound_key() -- all nil
local function get_table_for_ctx(ws)
local tbl = fetch_table(PLUGINS_NS, 0, DOWNSTREAM_PHASES_COUNT + 2)
if not tbl.initialized then
for i = 1, DOWNSTREAM_PHASES_COUNT do
tbl[DOWNSTREAM_PHASES[i]] = kong.table.new(ws.plugins[0] * 2, 1)
end
tbl.initialized = true
end
for i = 1, DOWNSTREAM_PHASES_COUNT do
tbl[DOWNSTREAM_PHASES[i]][0] = 0
end
tbl.ws = ws
return tbl
end
local function release(ctx)
local plugins = ctx.plugins
if plugins then
release_table(PLUGINS_NS, plugins, true)
ctx.plugins = nil
end
end
local function get_loaded_plugins()
return assert(kong.db.plugins:get_handlers())
end
local function get_configurable_plugins()
local i = 0
local plugins_with_configure_phase = {}
for _, plugin in ipairs(LOADED_PLUGINS) do
if plugin.handler[CONFIGURE_PHASE] then
i = i + 1
local name = plugin.name
plugins_with_configure_phase[name] = true
plugins_with_configure_phase[i] = plugin
end
end
return plugins_with_configure_phase
end
local function should_process_plugin(plugin)
if wasm.filters_by_name[plugin.name] then
return false
end
if plugin.enabled then
local c = constants.PROTOCOLS_WITH_SUBSYSTEM
for _, protocol in ipairs(plugin.protocols) do
if c[protocol] == subsystem then
return true
end
end
end
end
local function get_plugin_config(plugin, name, ws_id)
if not plugin or not plugin.enabled then
return
end
local cfg = plugin.config or {}
cfg.route_id = plugin.route and plugin.route.id
cfg.service_id = plugin.service and plugin.service.id
cfg.consumer_id = plugin.consumer and plugin.consumer.id
cfg.plugin_instance_name = plugin.instance_name
cfg.__plugin_id = plugin.id
cfg.__ws_id = ws_id
local key = kong.db.plugins:cache_key(name,
cfg.route_id,
cfg.service_id,
cfg.consumer_id,
nil,
ws_id)
-- TODO: deprecate usage of __key__ as id of plugin
if not cfg.__key__ then
cfg.__key__ = key
-- generate a unique sequence across workers
-- with a seq 0, plugin server generates an unused random instance id
local next_seq, err = ngx.shared.kong:incr("plugins_iterator:__seq__", 1, 0, 0)
if err then
next_seq = 0
end
cfg.__seq__ = next_seq
end
return cfg
end
---
-- Lookup a configuration for a given combination of route_id, service_id, consumer_id
--
-- The function checks various combinations of route_id, service_id and consumer_id to find
-- the best matching configuration in the given 'combos' table. The priority order is as follows:
--
-- 1. Route, Service, Consumer
-- 2. Route, Consumer
-- 3. Service, Consumer
-- 4. Route, Service
-- 5. Consumer
-- 6. Route
-- 7. Service
-- 8. Global
--
-- @function lookup_cfg
-- @tparam table combos A table containing configuration data indexed by compound keys.
-- @tparam string|nil route_id The route identifier.
-- @tparam string|nil service_id The service identifier.
-- @tparam string|nil consumer_id The consumer identifier.
-- @return any|nil The configuration corresponding to the best matching combination, or 'nil' if no configuration is found.
local function lookup_cfg(combos, route_id, service_id, consumer_id)
-- Use the build_compound_key function to create an index for the 'combos' table
if route_id and service_id and consumer_id then
local key = build_compound_key(route_id, service_id, consumer_id)
if combos[key] then
return combos[key]
end
end
if route_id and consumer_id then
local key = build_compound_key(route_id, nil, consumer_id)
if combos[key] then
return combos[key]
end
end
if service_id and consumer_id then
local key = build_compound_key(nil, service_id, consumer_id)
if combos[key] then
return combos[key]
end
end
if route_id and service_id then
local key = build_compound_key(route_id, service_id, nil)
if combos[key] then
return combos[key]
end
end
if consumer_id then
local key = build_compound_key(nil, nil, consumer_id)
if combos[key] then
return combos[key]
end
end
if route_id then
local key = build_compound_key(route_id, nil, nil)
if combos[key] then
return combos[key]
end
end
if service_id then
local key = build_compound_key(nil, service_id, nil)
if combos[key] then
return combos[key]
end
end
return combos[PLUGIN_GLOBAL_KEY]
end
---
-- Load the plugin configuration based on the context (route, service, and consumer) and plugin handler rules.
--
-- This function filters out route, service, and consumer information from the context based on the plugin handler rules,
-- and then calls the 'lookup_cfg' function to get the best matching plugin configuration for the given combination of
-- route_id, service_id, and consumer_id.
--
-- @function load_configuration_through_combos
-- @tparam table ctx A table containing the context information, including route, service, and authenticated_consumer.
-- @tparam table combos A table containing configuration data indexed by compound keys.
-- @tparam table plugin A table containing plugin information, including the handler with no_route, no_service, and no_consumer rules.
-- @treturn any|nil The configuration corresponding to the best matching combination, or 'nil' if no configuration is found.
local function load_configuration_through_combos(ctx, combos, plugin)
-- Filter out route, service, and consumer based on the plugin handler rules and get their ids
local handler = plugin.handler
local route_id = (not handler.no_route and ctx.route) and ctx.route.id or nil
local service_id = (not handler.no_service and ctx.service) and ctx.service.id or nil
local consumer_id = (not handler.no_consumer and ctx.authenticated_consumer) and ctx.authenticated_consumer.id or nil
-- Call the lookup_cfg function to get the best matching plugin configuration
return lookup_cfg(combos, route_id, service_id, consumer_id)
end
local function get_workspace(self, ctx)
if not ctx then
return self.ws[kong.default_workspace]
end
return self.ws[workspaces.get_workspace_id(ctx) or kong.default_workspace]
end
local function get_next_init_worker(plugins, i)
local i = i + 1
local plugin = plugins[i]
if not plugin then
return nil
end
if plugin.handler.init_worker then
return i, plugin
end
return get_next_init_worker(plugins, i)
end
local function get_init_worker_iterator(self)
if #self.loaded == 0 then
return nil
end
return get_next_init_worker, self.loaded
end
local function get_next_global_or_collected_plugin(plugins, i)
i = i + 2
if i > plugins[0] then
return nil
end
return i, plugins[i - 1], plugins[i]
end
local function get_global_iterator(self, phase)
local plugins = self.globals[phase]
local count = plugins and plugins[0] or 0
if count == 0 then
return nil
end
-- only execute this once per request
if phase == "certificate" or (phase == "rewrite" and var.https ~= "on") then
local i = 2
while i <= count do
kong.vault.update(plugins[i])
i = i + 2
end
end
return get_next_global_or_collected_plugin, plugins
end
local function get_collected_iterator(self, phase, ctx)
local plugins = ctx.plugins
if plugins then
plugins = plugins[phase]
if not plugins or plugins[0] == 0 then
return nil
end
return get_next_global_or_collected_plugin, plugins
end
return get_global_iterator(self, phase)
end
local function get_next_and_collect(ctx, i)
i = i + 1
local ws = ctx.plugins.ws
local plugins = ws.plugins
if i > plugins[0] then
return nil
end
local plugin = plugins[i]
local name = plugin.name
local cfg
-- Only pass combos for the plugin we're operating on
local combos = ws.combos[name]
if combos then
cfg = load_configuration_through_combos(ctx, combos, plugin)
if cfg then
kong.vault.update(cfg)
local handler = plugin.handler
local collected = ctx.plugins
for j = 1, DOWNSTREAM_PHASES_COUNT do
local phase = DOWNSTREAM_PHASES[j]
if handler[phase] then
local n = collected[phase][0] + 2
collected[phase][0] = n
collected[phase][n] = cfg
collected[phase][n - 1] = plugin
if phase == "response" and not ctx.buffered_proxying then
ctx.buffered_proxying = true
end
end
end
if handler[COLLECTING_PHASE] then
return i, plugin, cfg
end
end
end
return get_next_and_collect(ctx, i)
end
local function get_collecting_iterator(self, ctx)
local ws = get_workspace(self, ctx)
ctx.plugins = get_table_for_ctx(ws)
if not ws then
return nil
end
local plugins = ws.plugins
if plugins[0] == 0 then
return nil
end
return get_next_and_collect, ctx
end
local function new_ws_data()
return {
plugins = { [0] = 0 },
combos = {},
}
end
local function configure(configurable, ctx)
-- Disable hooks that are selectively enabled by plugins
-- in their :configure handler
req_dyn_hook_disable_by_default("observability_logs")
ctx = ctx or ngx.ctx
local kong_global = require "kong.global"
for _, plugin in ipairs(CONFIGURABLE_PLUGINS) do
local name = plugin.name
kong_global.set_namespaced_log(kong, plugin.name, ctx)
local start = get_updated_monotonic_ms()
local ok, err = pcall(plugin.handler[CONFIGURE_PHASE], plugin.handler, configurable[name])
local elapsed = get_updated_monotonic_ms() - start
kong_global.reset_log(kong, ctx)
if not ok then
kong.log.err("failed to execute plugin '", name, ":", CONFIGURE_PHASE, " (", err, ")")
else
if elapsed > 50 then
kong.log.notice("executing plugin '", name, ":", CONFIGURE_PHASE, " took excessively long: ", elapsed, " ms")
end
end
end
end
local function create_configure(configurable)
-- we only want the plugin_iterator:configure to be only available on proxying
-- nodes (or data planes), thus we disable it if this code gets executed on control
-- plane or on a node that does not listen any proxy ports.
--
-- TODO: move to PDK, e.g. kong.node.is_proxying()
if kong.configuration.role == "control_plane"
or ((subsystem == "http" and #kong.configuration.proxy_listeners == 0) or
(subsystem == "stream" and #kong.configuration.stream_listeners == 0))
then
return function() end
end
return function(self, ctx)
configure(configurable, ctx)
-- self destruct the function so that it cannot be called twice
-- if it ever happens to be called twice, it should be very visible
-- because of this.
self.configure = nil
configurable = nil
end
end
function PluginsIterator.new(version)
local is_not_dbless = kong.db.strategy ~= "off"
if is_not_dbless then
if not version then
error("version must be given", 2)
end
end
LOADED_PLUGINS = LOADED_PLUGINS or get_loaded_plugins()
CONFIGURABLE_PLUGINS = CONFIGURABLE_PLUGINS or get_configurable_plugins()
ENABLED_PLUGINS = ENABLED_PLUGINS or kong.configuration.loaded_plugins
local ws_id = workspaces.get_workspace_id() or kong.default_workspace
local ws = {
[ws_id] = new_ws_data()
}
local counter = 0
local globals
do
globals = {}
for _, phase in ipairs(NON_COLLECTING_PHASES) do
globals[phase] = { [0] = 0 }
end
end
local configurable = {}
local has_plugins = false
local page_size = kong.db.plugins.pagination.max_page_size
if kong.configuration.role == "control_plane" then
goto done
end
for plugin, err in kong.db.plugins:each(page_size, GLOBAL_QUERY_OPTS) do
if err then
return nil, err
end
local name = plugin.name
if not ENABLED_PLUGINS[name] then
return nil, name .. " plugin is in use but not enabled"
end
if is_not_dbless and counter > 0 and counter % page_size == 0 and kong.core_cache then
local new_version, err = kong.core_cache:get("plugins_iterator:version", TTL_ZERO, uuid)
if err then
return nil, "failed to retrieve plugins iterator version: " .. err
end
if new_version ~= version then
-- the plugins iterator rebuild is being done by a different process at
-- the same time, stop here and let the other one go for it
kong.log.info("plugins iterator was changed while rebuilding it")
return
end
end
if should_process_plugin(plugin) then
-- Get the plugin configuration for the specified workspace (ws_id)
local cfg = get_plugin_config(plugin, name, plugin.ws_id)
if cfg then
has_plugins = true
if CONFIGURABLE_PLUGINS[name] then
configurable[name] = configurable[name] or {}
configurable[name][#configurable[name] + 1] = cfg
end
local data = ws[plugin.ws_id]
if not data then
data = new_ws_data()
ws[plugin.ws_id] = data
end
local plugins = data.plugins
local combos = data.combos
plugins[name] = true
-- Retrieve route_id, service_id, and consumer_id from the plugin object, if they exist
local route_id = plugin.route and plugin.route.id
local service_id = plugin.service and plugin.service.id
local consumer_id = plugin.consumer and plugin.consumer.id
-- Determine if the plugin configuration is global (i.e., not tied to any route, service, consumer or group)
if not (route_id or service_id or consumer_id) and plugin.ws_id == kong.default_workspace then
-- Store the global configuration for the plugin in the 'globals' table
globals[name] = cfg
end
-- Initialize an empty table for the plugin in the 'combos' table if it doesn't already exist
combos[name] = combos[name] or {}
-- Build a compound key using the route_id, service_id, and consumer_id
local compound_key = build_compound_key(route_id, service_id, consumer_id)
-- Store the plugin configuration in the 'combos' table using the compound key
combos[name][compound_key] = cfg
end
end
counter = counter + 1
end
if has_plugins then
-- loaded_plugins contains all the plugins that we _may_ execute
for _, plugin in ipairs(LOADED_PLUGINS) do
local name = plugin.name
-- ws contains all the plugins that are associated to the request via route/service/global mappings
for _, data in pairs(ws) do
local plugins = data.plugins
if plugins[name] then -- is the plugin associated to the request(workspace/route/service)?
local n = plugins[0] + 1
plugins[n] = plugin -- next item goes into next slot
plugins[0] = n -- index 0 holds table size
plugins[name] = nil -- remove the placeholder value
end
end
local cfg = globals[name]
if cfg then
for _, phase in ipairs(NON_COLLECTING_PHASES) do
if plugin.handler[phase] then
local plugins = globals[phase]
local n = plugins[0] + 2
plugins[0] = n
plugins[n] = cfg
plugins[n - 1] = plugin
end
end
end
end
end
::done::
return {
version = version,
ws = ws,
loaded = LOADED_PLUGINS,
configure = create_configure(configurable),
globals = globals,
get_init_worker_iterator = get_init_worker_iterator,
get_global_iterator = get_global_iterator,
get_collecting_iterator = get_collecting_iterator,
get_collected_iterator = get_collected_iterator,
has_plugins = has_plugins,
release = release,
}
end
-- for testing
PluginsIterator.lookup_cfg = lookup_cfg
PluginsIterator.build_compound_key = build_compound_key
return PluginsIterator