Skip to content

Commit

Permalink
feat(wasm): add wasm plugin interface
Browse files Browse the repository at this point in the history
  • Loading branch information
hishamhm authored and flrgh committed Nov 7, 2024
1 parent 713908f commit ffa8f69
Show file tree
Hide file tree
Showing 14 changed files with 713 additions and 137 deletions.
3 changes: 3 additions & 0 deletions changelog/unreleased/kong/wasm-filter-plugins.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: "**proxy-wasm**: Added support for Wasm filters to be configured via the /plugins admin API"
type: feature
scope: Core
2 changes: 2 additions & 0 deletions kong-3.9.0-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ build = {
["kong.runloop.plugin_servers.mp_rpc"] = "kong/runloop/plugin_servers/mp_rpc.lua",
["kong.runloop.plugin_servers.pb_rpc"] = "kong/runloop/plugin_servers/pb_rpc.lua",
["kong.runloop.wasm"] = "kong/runloop/wasm.lua",
["kong.runloop.wasm.plugins"] = "kong/runloop/wasm/plugins.lua",
["kong.runloop.wasm.properties"] = "kong/runloop/wasm/properties.lua",

["kong.workspaces"] = "kong/workspaces/init.lua",
Expand Down Expand Up @@ -281,6 +282,7 @@ build = {
["kong.db.schema.json"] = "kong/db/schema/json.lua",
["kong.db.schema.others.migrations"] = "kong/db/schema/others/migrations.lua",
["kong.db.schema.others.declarative_config"] = "kong/db/schema/others/declarative_config.lua",
["kong.db.schema.others.wasm_filter"] = "kong/db/schema/others/wasm_filter.lua",
["kong.db.schema.entity"] = "kong/db/schema/entity.lua",
["kong.db.schema.metaschema"] = "kong/db/schema/metaschema.lua",
["kong.db.schema.typedefs"] = "kong/db/schema/typedefs.lua",
Expand Down
10 changes: 9 additions & 1 deletion kong/conf_loader/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ local function load(path, custom_conf, opts)

---@type kong.configuration.wasm_filter[]
local active_filters = {}
for _, filter in pairs(active_filters_by_name) do
for name, filter in pairs(active_filters_by_name) do
insert(active_filters, filter)
end
sort(active_filters, function(lhs, rhs)
Expand Down Expand Up @@ -755,6 +755,14 @@ local function load(path, custom_conf, opts)
end
end

if conf.wasm_modules_parsed then
for _, filter in ipairs(conf.wasm_modules_parsed) do
assert(plugins[filter.name] == nil,
"duplicate plugin/wasm filter name: " .. filter.name)
plugins[filter.name] = true
end
end

conf.loaded_plugins = setmetatable(plugins, conf_constants._NOP_TOSTRING_MT)
end

Expand Down
8 changes: 8 additions & 0 deletions kong/db/dao/plugins.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local DAO = require "kong.db.dao"
local plugin_loader = require "kong.db.schema.plugin_loader"
local reports = require "kong.reports"
local plugin_servers = require "kong.runloop.plugin_servers"
local wasm_plugins = require "kong.runloop.wasm.plugins"
local version = require "version"
local load_module_if_exists = require "kong.tools.module".load_module_if_exists

Expand Down Expand Up @@ -167,6 +168,13 @@ local load_plugin_handler do
end
end

if not ok then
ok, handler = wasm_plugins.load_plugin(plugin)
if type(handler) == "table" then
handler._wasm = true
end
end

if not ok then
return nil, plugin .. " plugin is enabled but not installed;\n" .. handler
end
Expand Down
59 changes: 3 additions & 56 deletions kong/db/schema/entities/filter_chains.lua
Original file line number Diff line number Diff line change
@@ -1,74 +1,21 @@
local typedefs = require "kong.db.schema.typedefs"
local filter = require "kong.db.schema.others.wasm_filter"
local wasm = require "kong.runloop.wasm"
local constants = require "kong.constants"
local json_schema = require "kong.db.schema.json"


---@class kong.db.schema.entities.filter_chain : table
---
---@field id string
---@field name string|nil
---@field enabled boolean
---@field route table|nil
---@field service table|nil
---@field route { id: string }|nil
---@field service { id: string }|nil
---@field created_at number
---@field updated_at number
---@field tags string[]
---@field filters kong.db.schema.entities.wasm_filter[]


---@class kong.db.schema.entities.wasm_filter : table
---
---@field name string
---@field enabled boolean
---@field config any|nil


local filter_config_schema = {
parent_subschema_key = "name",
namespace = constants.SCHEMA_NAMESPACES.PROXY_WASM_FILTERS,
optional = true,
default = {
["$schema"] = json_schema.DRAFT_4,
-- filters with no user-defined JSON schema may accept an optional
-- config, but only as a string
type = { "string", "null" },
},
}


if kong and kong.configuration and kong.configuration.role == "data_plane" then
-- data plane nodes are not guaranteed to have access to filter metadata, so
-- they will use a JSON schema that permits all data types
--
-- this branch can be removed if we decide to turn off entity validation in
-- the data plane altogether
filter_config_schema = {
inline = {
["$schema"] = json_schema.DRAFT_4,
type = { "array", "boolean", "integer", "null", "number", "object", "string" },
},
}
end


local filter = {
type = "record",
fields = {
{ name = { type = "string", required = true, one_of = wasm.filter_names,
err = "no such filter", }, },
{ enabled = { type = "boolean", default = true, required = true, }, },

{ config = {
type = "json",
required = false,
json_schema = filter_config_schema,
},
},

},
}

return {
name = "filter_chains",
primary_key = { "id" },
Expand Down
74 changes: 48 additions & 26 deletions kong/db/schema/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ local random_string = require("kong.tools.rand").random_string
local uuid = require("kong.tools.uuid").uuid
local json_validate = json.validate

local EMPTY = {}

local Schema = {}
Schema.__index = Schema
Expand Down Expand Up @@ -1076,6 +1077,9 @@ end
-- @return true if compatible, false otherwise.
local function compatible_fields(f1, f2)
local t1, t2 = f1.type, f2.type
if t1 == "record" and t2 == "json" then
return true
end
if t1 ~= t2 then
return false
end
Expand Down Expand Up @@ -1128,6 +1132,44 @@ local function resolve_field(self, k, field, subschema)
end


local function get_json_schema(self, input)
local json_schema = self.json_schema

local schema = json_schema.inline
if schema then
return schema
end

local parent_key = json_schema.parent_subschema_key
local subschema_key = input[parent_key]

if subschema_key then
local schema_name = json_schema.namespace .. "/" .. subschema_key
schema = json.get_schema(schema_name) or json_schema.default

if schema then
return schema

elseif not json_schema.optional then
return validation_errors.JSON_SCHEMA_NOT_FOUND:format(schema_name)
end

elseif not json_schema.optional then
return validation_errors.JSON_PARENT_KEY_MISSING:format(subschema_key, parent_key)
end
end


local function validate_json_field(field, field_name, input)
local schema, err = get_json_schema(field, input)
if schema then
local _
_, err = json_validate(input[field_name], schema)
end
return err
end


--- Validate fields of a table, individually, against the schema.
-- @param self The schema
-- @param input The input table.
Expand All @@ -1141,37 +1183,17 @@ validate_fields = function(self, input)
local errors, _ = {}

local subschema = get_subschema(self, input)
local subschema_fields = subschema and subschema.fields or EMPTY

for k, v in pairs(input) do
local err
local field = self.fields[tostring(k)]
local subschema_field = subschema_fields[tostring(k)]

if field and field.type == "json" then
local json_schema = field.json_schema
local inline_schema = json_schema.inline

if inline_schema then
_, errors[k] = json_validate(v, inline_schema)

else
local parent_key = json_schema.parent_subschema_key
local json_subschema_key = input[parent_key]

if json_subschema_key then
local schema_name = json_schema.namespace .. "/" .. json_subschema_key
inline_schema = json.get_schema(schema_name) or json_schema.default

if inline_schema then
_, errors[k] = json_validate(v, inline_schema)

elseif not json_schema.optional then
errors[k] = validation_errors.JSON_SCHEMA_NOT_FOUND:format(schema_name)
end

elseif not json_schema.optional then
errors[k] = validation_errors.JSON_PARENT_KEY_MISSING:format(k, parent_key)
end
end
if field and field.type == "json"
or (subschema_field and subschema_field.type == "json")
then
errors[k] = validate_json_field(subschema_field or field, k, input)

elseif field and field.type == "self" then
local pok
Expand Down
59 changes: 59 additions & 0 deletions kong/db/schema/others/wasm_filter.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
local constants = require "kong.constants"
local json_schema = require "kong.db.schema.json"
local wasm = require "kong.runloop.wasm"


---@class kong.db.schema.entities.wasm_filter : table
---
---@field name string
---@field enabled boolean
---@field config any|nil


local filter_config_schema = {
parent_subschema_key = "name",
namespace = constants.SCHEMA_NAMESPACES.PROXY_WASM_FILTERS,
optional = true,
default = {
["$schema"] = json_schema.DRAFT_4,
-- filters with no user-defined JSON schema may accept an optional
-- config, but only as a string
type = { "string", "null" },
},
}


-- FIXME: this is clunky and error-prone because a harmless refactor might
-- affect whether this file is require()-ed before or after `kong.configuration`
-- is initialized
if kong and kong.configuration and kong.configuration.role == "data_plane" then
-- data plane nodes are not guaranteed to have access to filter metadata, so
-- they will use a JSON schema that permits all data types
--
-- this branch can be removed if we decide to turn off entity validation in
-- the data plane altogether
filter_config_schema = {
inline = {
["$schema"] = json_schema.DRAFT_4,
type = { "array", "boolean", "integer", "null", "number", "object", "string" },
},
}
end


return {
type = "record",
fields = {
{ name = { type = "string", required = true, one_of = wasm.filter_names,
err = "no such filter", }, },
{ enabled = { type = "boolean", default = true, required = true, }, },

{ config = {
type = "json",
required = false,
json_schema = filter_config_schema,
},
},

},
}
4 changes: 4 additions & 0 deletions kong/db/schema/plugin_loader.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local MetaSchema = require "kong.db.schema.metaschema"
local Entity = require "kong.db.schema.entity"
local plugin_servers = require "kong.runloop.plugin_servers"
local wasm_plugins = require "kong.runloop.wasm.plugins"
local is_array = require "kong.tools.table".is_array
local load_module_if_exists = require "kong.tools.module".load_module_if_exists

Expand All @@ -18,6 +19,9 @@ function plugin_loader.load_subschema(parent_schema, plugin, errors)
if not ok then
ok, schema = plugin_servers.load_schema(plugin)
end
if not ok then
ok, schema = wasm_plugins.load_schema(plugin)
end

if not ok then
return nil, "no configuration schema found for plugin: " .. plugin
Expand Down
26 changes: 19 additions & 7 deletions kong/runloop/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -312,15 +312,26 @@ local function crud_wasm_handler(data, schema_name)
return
end

-- cache is invalidated on service/route deletion to ensure we don't
-- have oprhaned filter chain data cached
local is_delete = data.operation == "delete"
and (schema_name == "services"
or schema_name == "routes")
local invalidate = false

local updated = schema_name == "filter_chains" or is_delete
-- always invalidate for filter_chain entity changes
if schema_name == "filter_chains" then
invalidate = true

if updated then
-- invalidate on service/route deletion to ensure we don't have any orphaned
-- filter chain data cached
elseif schema_name == "services" or schema_name == "routes" then
invalidate = data.operation == "delete"

-- invalidate for changes to wasm filter plugin entities
elseif schema_name == "plugins" then
local new_name = data.entity.name
local old_name = data.old_entity and data.old_entity.name
invalidate = (new_name and wasm.filters_by_name[new_name]) or
(old_name and wasm.filters_by_name[old_name])
end

if invalidate then
log(DEBUG, "[events] wasm filter chains updated, invalidating cache")
core_cache:invalidate("filter_chains:version")
end
Expand Down Expand Up @@ -396,6 +407,7 @@ local LOCAL_HANDLERS = {
{ "crud" , "filter_chains" , crud_wasm_handler },
{ "crud" , "services" , crud_wasm_handler },
{ "crud" , "routes" , crud_wasm_handler },
{ "crud" , "plugins" , crud_wasm_handler },

-- ca certificate store caches invalidations
{ "crud" , "ca_certificates" , crud_ca_certificates_handler },
Expand Down
5 changes: 5 additions & 0 deletions kong/runloop/plugins_iterator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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
Expand Down Expand Up @@ -138,6 +139,10 @@ 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
Expand Down
Loading

0 comments on commit ffa8f69

Please sign in to comment.