Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wasm): support bundled filters #12843

Merged
merged 1 commit into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/unreleased/kong/wasm-bundled-filters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: Add `wasm_filters` configuration value for enabling individual filters
flrgh marked this conversation as resolved.
Show resolved Hide resolved
type: feature
scope: Configuration
27 changes: 27 additions & 0 deletions kong.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,33 @@
# top level are registered
# * This path _may_ be a symlink to a directory.

#wasm_filters = bundled,user # Comma-separated list of Wasm filters to be made
# available for use in filter chains.
#
# When the `off` keyword is specified as the
# only value, no filters will be available for use.
#
# When the `bundled` keyword is specified, all filters
# bundled with Kong will be available.
#
# When the `user` keyword is specified, all filters
# within the `wasm_filters_path` will be available.
#
# **Examples:**
#
# - `wasm_filters = bundled,user` enables _all_ bundled
# and user-supplied filters
# - `wasm_filters = user` enables _only_ user-supplied
# filters
# - `wasm_filters = filter-a,filter-b` enables _only_
# filters named `filter-a` or `filter-b` (whether
# bundled _or_ user-suppplied)
#
# If a conflict occurs where a bundled filter and a
# user-supplied filter share the same name, a warning
# will be logged, and the user-supplied filter will
# be used instead.

#------------------------------------------------------------------------------
# WASM injected directives
#------------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions kong/conf_loader/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ local CONF_PARSERS = {

wasm = { typ = "boolean" },
wasm_filters_path = { typ = "string" },
wasm_filters = { typ = "array" },

error_template_html = { typ = "string" },
error_template_json = { typ = "string" },
Expand Down Expand Up @@ -640,4 +641,6 @@ return {
_NOP_TOSTRING_MT = _NOP_TOSTRING_MT,

LMDB_VALIDATION_TAG = LMDB_VALIDATION_TAG,

WASM_BUNDLED_FILTERS_PATH = "/usr/local/kong/wasm",
}
88 changes: 84 additions & 4 deletions kong/conf_loader/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local socket_url = require "socket.url"
local conf_constants = require "kong.conf_loader.constants"
local listeners = require "kong.conf_loader.listeners"
local conf_parse = require "kong.conf_loader.parse"
local nginx_signals = require "kong.cmd.utils.nginx_signals"
local pl_pretty = require "pl.pretty"
local pl_config = require "pl.config"
local pl_file = require "pl.file"
Expand Down Expand Up @@ -171,11 +172,12 @@ local function load_config_file(path)
end

--- Get available Wasm filters list
-- @param[type=string] Path where Wasm filters are stored.
---@param filters_path string # Path where Wasm filters are stored.
---@return kong.configuration.wasm_filter[]
local function get_wasm_filters(filters_path)
local wasm_filters = {}

if filters_path then
if filters_path and pl_path.isdir(filters_path) then
local filter_files = {}
for entry in pl_path.dir(filters_path) do
local pathname = pl_path.join(filters_path, entry)
Expand Down Expand Up @@ -547,8 +549,86 @@ local function load(path, custom_conf, opts)

-- Wasm module support
if conf.wasm then
local wasm_filters = get_wasm_filters(conf.wasm_filters_path)
conf.wasm_modules_parsed = setmetatable(wasm_filters, conf_constants._NOP_TOSTRING_MT)
---@type table<string, boolean>
local allowed_filter_names = {}
local all_bundled_filters_enabled = false
local all_user_filters_enabled = false
local all_filters_disabled = false
for _, filter in ipairs(conf.wasm_filters) do
if filter == "bundled" then
all_bundled_filters_enabled = true

elseif filter == "user" then
all_user_filters_enabled = true

elseif filter == "off" then
all_filters_disabled = true

else
allowed_filter_names[filter] = true
end
end

if all_filters_disabled then
allowed_filter_names = {}
all_bundled_filters_enabled = false
all_user_filters_enabled = false
end

---@type table<string, kong.configuration.wasm_filter>
local active_filters_by_name = {}

local bundled_filter_path = conf_constants.WASM_BUNDLED_FILTERS_PATH
if not pl_path.isdir(bundled_filter_path) then
local alt_path

local nginx_bin = nginx_signals.find_nginx_bin(conf)
if nginx_bin then
alt_path = pl_path.dirname(nginx_bin) .. "/../../../kong/wasm"
alt_path = pl_path.normpath(alt_path) or alt_path
end

if alt_path and pl_path.isdir(alt_path) then
log.debug("loading bundled proxy-wasm filters from alt path: %s",
alt_path)
bundled_filter_path = alt_path

else
log.warn("Bundled proxy-wasm filters path (%s) does not exist " ..
"or is not a directory. Bundled filters may not be " ..
"available", bundled_filter_path)
end
end

conf.wasm_bundled_filters_path = bundled_filter_path
local bundled_filters = get_wasm_filters(bundled_filter_path)
for _, filter in ipairs(bundled_filters) do
if all_bundled_filters_enabled or allowed_filter_names[filter.name] then
active_filters_by_name[filter.name] = filter
end
end

local user_filters = get_wasm_filters(conf.wasm_filters_path)
for _, filter in ipairs(user_filters) do
if all_user_filters_enabled or allowed_filter_names[filter.name] then
if active_filters_by_name[filter.name] then
log.warn("Replacing bundled filter %s with a user-supplied " ..
"filter at %s", filter.name, filter.path)
end
active_filters_by_name[filter.name] = filter
end
end

---@type kong.configuration.wasm_filter[]
local active_filters = {}
for _, filter in pairs(active_filters_by_name) do
insert(active_filters, filter)
end
sort(active_filters, function(lhs, rhs)
return lhs.name < rhs.name
end)

conf.wasm_modules_parsed = setmetatable(active_filters, conf_constants._NOP_TOSTRING_MT)

local function add_wasm_directive(directive, value, prefix)
local directive_name = (prefix or "") .. directive
Expand Down
2 changes: 1 addition & 1 deletion kong/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ local constants = {
exit = "kong",
service = "upstream",
}
}
},
}

for _, v in ipairs(constants.CLUSTERING_SYNC_STATUS) do
Expand Down
1 change: 1 addition & 0 deletions kong/templates/kong_defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ tracing_sampling_rate = 0.01
wasm = off
wasm_filters_path = NONE
wasm_dynamic_module = NONE
wasm_filters = bundled,user

request_debug = on
request_debug_token =
Expand Down
122 changes: 115 additions & 7 deletions spec/01-unit/03-conf_loader_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1948,10 +1948,47 @@ describe("Configuration loader", function()

describe("#wasm properties", function()
local temp_dir, cleanup
local user_filters
local bundled_filters
local all_filters

lazy_setup(function()
temp_dir, cleanup = helpers.make_temp_dir()
assert(helpers.file.write(temp_dir .. "/empty-filter.wasm", "hello!"))
assert(helpers.file.write(temp_dir .. "/filter-a.wasm", "hello!"))
assert(helpers.file.write(temp_dir .. "/filter-b.wasm", "hello!"))

user_filters = {
{
name = "filter-a",
path = temp_dir .. "/filter-a.wasm",
},
{
name = "filter-b",
path = temp_dir .. "/filter-b.wasm",
}
}

do
-- for local builds, the bundled filter path is not constant, so we
-- must load the config first to discover the path
local conf = assert(conf_loader(nil, {
wasm = "on",
wasm_filters = "bundled",
}))

assert(conf.wasm_bundled_filters_path)
bundled_filters = {
{
name = "datakit",
path = conf.wasm_bundled_filters_path .. "/datakit.wasm",
},
}
end

all_filters = {}
table.insert(all_filters, bundled_filters[1])
table.insert(all_filters, user_filters[1])
table.insert(all_filters, user_filters[2])
end)

lazy_teardown(function() cleanup() end)
Expand Down Expand Up @@ -1979,12 +2016,7 @@ describe("Configuration loader", function()
wasm_filters_path = temp_dir,
})
assert.is_nil(err)
assert.same({
{
name = "empty-filter",
path = temp_dir .. "/empty-filter.wasm",
}
}, conf.wasm_modules_parsed)
assert.same(all_filters, conf.wasm_modules_parsed)
assert.same(temp_dir, conf.wasm_filters_path)
end)

Expand All @@ -1997,6 +2029,82 @@ describe("Configuration loader", function()
assert.is_nil(conf)
end)

it("wasm_filters default", function()
local conf, err = conf_loader(nil, {
wasm = "on",
wasm_filters_path = temp_dir,
})
assert.is_nil(err)
assert.same(all_filters, conf.wasm_modules_parsed)
assert.same({ "bundled", "user" }, conf.wasm_filters)
end)

it("wasm_filters = off", function()
local conf, err = conf_loader(nil, {
wasm = "on",
wasm_filters = "off",
wasm_filters_path = temp_dir,
})
assert.is_nil(err)
assert.same({}, conf.wasm_modules_parsed)
end)

it("wasm_filters = 'user' allows all user filters", function()
local conf, err = conf_loader(nil, {
wasm = "on",
wasm_filters = "user",
wasm_filters_path = temp_dir,
})
assert.is_nil(err)
assert.same(user_filters, conf.wasm_modules_parsed)
end)

it("wasm_filters can allow individual user filters", function()
local conf, err = conf_loader(nil, {
wasm = "on",
wasm_filters = assert(user_filters[1].name),
wasm_filters_path = temp_dir,
})
assert.is_nil(err)
assert.same({ user_filters[1] }, conf.wasm_modules_parsed)
end)

it("wasm_filters = 'bundled' allows all bundled filters", function()
local conf, err = conf_loader(nil, {
wasm = "on",
wasm_filters = "bundled",
wasm_filters_path = temp_dir,
})
assert.is_nil(err)
assert.same(bundled_filters, conf.wasm_modules_parsed)
end)

it("prefers user filters to bundled filters when a conflict exists", function()
local user_filter = temp_dir .. "/datakit.wasm"
assert(helpers.file.write(user_filter, "I'm a happy little wasm filter"))
finally(function()
assert(os.remove(user_filter))
end)

local conf, err = conf_loader(nil, {
wasm = "on",
wasm_filters = "bundled,user",
wasm_filters_path = temp_dir,
})
assert.is_nil(err)

local found = false
for _, filter in ipairs(conf.wasm_modules_parsed) do
if filter.name == "datakit" then
found = true
assert.equals(user_filter, filter.path,
"user filter should override the bundled filter")
end
end

assert.is_true(found, "expected the user filter to be enabled")
end)

end)

describe("errors", function()
Expand Down
18 changes: 18 additions & 0 deletions spec/02-integration/20-wasm/06-clustering_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe("#wasm - hybrid mode #postgres", function()
local cp_filter_path

local dp_prefix = "dp"
local dp_errlog = dp_prefix .. "/logs/error.log"

lazy_setup(function()
helpers.clean_prefix(cp_prefix)
Expand Down Expand Up @@ -108,8 +109,16 @@ describe("#wasm - hybrid mode #postgres", function()
cluster_listen = "127.0.0.1:9005",
nginx_conf = "spec/fixtures/custom_nginx.template",
wasm = true,
wasm_filters = "user", -- don't enable bundled filters for this test
wasm_filters_path = cp_filter_path,
nginx_main_worker_processes = 2,
}))

assert.logfile(cp_errlog).has.line([[successfully loaded "response_transformer" module]], true, 10)
assert.logfile(cp_errlog).has.no.line("[error]", true, 0)
assert.logfile(cp_errlog).has.no.line("[alert]", true, 0)
assert.logfile(cp_errlog).has.no.line("[crit]", true, 0)
assert.logfile(cp_errlog).has.no.line("[emerg]", true, 0)
end)

lazy_teardown(function()
Expand Down Expand Up @@ -138,10 +147,18 @@ describe("#wasm - hybrid mode #postgres", function()
admin_listen = "off",
nginx_conf = "spec/fixtures/custom_nginx.template",
wasm = true,
wasm_filters = "user", -- don't enable bundled filters for this test
wasm_filters_path = dp_filter_path,
node_id = node_id,
nginx_main_worker_processes = 2,
}))

assert.logfile(dp_errlog).has.line([[successfully loaded "response_transformer" module]], true, 10)
assert.logfile(dp_errlog).has.no.line("[error]", true, 0)
assert.logfile(dp_errlog).has.no.line("[alert]", true, 0)
assert.logfile(dp_errlog).has.no.line("[crit]", true, 0)
assert.logfile(dp_errlog).has.no.line("[emerg]", true, 0)

client = helpers.proxy_client()
end)

Expand Down Expand Up @@ -325,6 +342,7 @@ describe("#wasm - hybrid mode #postgres", function()
admin_listen = "off",
nginx_conf = "spec/fixtures/custom_nginx.template",
wasm = true,
wasm_filters = "user", -- don't enable bundled filters for this test
wasm_filters_path = tmp_dir,
node_id = node_id,
}))
Expand Down
2 changes: 1 addition & 1 deletion spec/02-integration/20-wasm/07-reports_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ for _, strategy in helpers.each_strategy() do
local _, reports_data = assert(reports_server:join())
reports_data = cjson.encode(reports_data)

assert.match("wasm_cnt=2", reports_data)
assert.match("wasm_cnt=3", reports_data)
end)

it("logs number of requests triggering a Wasm filter", function()
Expand Down
Loading
Loading