Skip to content

Commit

Permalink
feat(wasmx): DB entities and admin API (#10317)
Browse files Browse the repository at this point in the history
  • Loading branch information
flrgh authored and locao committed Jul 14, 2023
1 parent 2e21534 commit 61c8671
Show file tree
Hide file tree
Showing 5 changed files with 685 additions and 0 deletions.
1 change: 1 addition & 0 deletions kong/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ local constants = {
"vaults",
"key_sets",
"keys",
"wasm_filter_chains",
},
ENTITY_CACHE_STORE = setmetatable({
consumers = "cache",
Expand Down
42 changes: 42 additions & 0 deletions kong/db/migrations/core/019_320_to_330.lua
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,48 @@ return {
EXCEPTION WHEN UNDEFINED_COLUMN OR UNDEFINED_TABLE THEN
-- Do nothing, accept existing state
END$$;
CREATE TABLE IF NOT EXISTS "wasm_filter_chains" (
"id" UUID PRIMARY KEY,
"name" TEXT UNIQUE,
"enabled" BOOLEAN DEFAULT TRUE,
"route_id" UUID REFERENCES "routes" ("id") ON DELETE CASCADE,
"service_id" UUID REFERENCES "services" ("id") ON DELETE CASCADE,
"ws_id" UUID REFERENCES "workspaces" ("id") ON DELETE CASCADE,
"protocols" TEXT[],
"filters" JSONB[],
"tags" TEXT[],
"created_at" TIMESTAMP WITH TIME ZONE,
"updated_at" TIMESTAMP WITH TIME ZONE
);
DO $$
BEGIN
CREATE INDEX IF NOT EXISTS "wasm_filter_chains_name_idx" ON "wasm_filter_chains" ("name");
EXCEPTION WHEN UNDEFINED_COLUMN then
-- do nothing, accept existing state
END$$;
DO $$
BEGIN
CREATE INDEX IF NOT EXISTS "wasm_filter_chains_tags_idx" ON "wasm_filter_chains" USING GIN ("tags");
EXCEPTION WHEN UNDEFINED_COLUMN then
-- do nothing, accept existing state
END$$;
DROP TRIGGER IF EXISTS "wasm_filter_chains_sync_tags_trigger" ON "wasm_filter_chains";
DO $$
BEGIN
CREATE TRIGGER "wasm_filter_chains_sync_tags_trigger"
AFTER INSERT OR UPDATE OF "tags"
OR DELETE ON "wasm_filter_chains"
FOR EACH ROW
EXECUTE PROCEDURE "sync_tags" ();
EXCEPTION WHEN undefined_column OR undefined_table THEN
-- do nothing, accept existing state
END$$;
]],
},
}
53 changes: 53 additions & 0 deletions kong/db/schema/entities/wasm_filter_chains.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
local typedefs = require "kong.db.schema.typedefs"

---@class kong.db.schema.entities.wasm_filter_chain : table
---
---@field id string
---@field name string|nil
---@field enabled boolean
---@field route table|nil
---@field service table|nil
---@field protocols table|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 string|table|nil

local filter = {
type = "record",
fields = {
{ name = { type = "string", required = true, }, },
{ config = { type = "string", required = false, }, },
{ enabled = { type = "boolean", default = true, required = true, }, },
},
}


return {
name = "wasm_filter_chains",
primary_key = { "id" },
endpoint_key = "name",
admin_api_name = "wasm/filter-chains",
generate_admin_api = true,
workspaceable = true,

fields = {
{ id = typedefs.uuid },
{ name = typedefs.utf8_name { required = false, unique = true }, },
{ enabled = { type = "boolean", required = true, default = true, }, },
{ route = { type = "foreign", reference = "routes", default = ngx.null, on_delete = "cascade", }, },
{ service = { type = "foreign", reference = "services", default = ngx.null, on_delete = "cascade", }, },
{ protocols = typedefs.protocols },
{ filters = { type = "array", required = true, elements = filter, len_min = 1, } },
{ created_at = typedefs.auto_timestamp_s },
{ updated_at = typedefs.auto_timestamp_s },
{ tags = typedefs.tags },
},
}
273 changes: 273 additions & 0 deletions spec/02-integration/15-wasm/01-admin-api_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
local helpers = require "spec.helpers"
local utils = require "kong.tools.utils"

-- no cassandra support
for _, strategy in helpers.each_strategy({ "postgres" }) do

describe("WASMX admin API [#" .. strategy .. "]", function()
local admin
local db

lazy_setup(function()
local _
_, db = helpers.get_db_utils(strategy, {
"routes",
"services",
"wasm_filter_chains",
})

helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
wasmx = true,
})

admin = helpers.admin_client()
end)

lazy_teardown(function()
helpers.stop_kong(nil, true)
if admin then admin:close() end
end)


local function reset_db()
db.wasm_filter_chains:truncate()
end


local function unsupported(method, path)
describe(method, function()
it("is not supported", function()
local res = assert(admin:send {
method = method,
path = path,
})
assert.response(res).has.status(405)
end)
end)
end


local function json(body)
return {
headers = { ["Content-Type"] = "application/json" },
body = body,
}
end


describe("/wasm/filter-chains", function()
before_each(reset_db)

describe("POST", function()
it("creates a filter chain", function()
local res = admin:post("/wasm/filter-chains", {
headers = { ["Content-Type"] = "application/json" },
body = {
name = "test",
filters = { { name = "test" } },
},
})

assert.response(res).has.status(201)
local body = assert.response(res).has.jsonbody()

assert.is_string(body.id)
assert.truthy(utils.is_valid_uuid(body.id))

assert.equals("test", body.name)
assert.equals(1, #body.filters)
assert.equals("test", body.filters[1].name)
end)
end)

describe("GET", function()
it("returns a collection of filter chains", function()
local res = admin:get("/wasm/filter-chains")
assert.response(res).has.status(200)

local body = assert.response(res).has.jsonbody()
assert.same({ data = {}, next = ngx.null }, body)

res = admin:post("/wasm/filter-chains", json {
name = "test",
filters = { { name = "test" } },
})

assert.response(res).has.status(201)
local chain = assert.response(res).has.jsonbody()

res = admin:get("/wasm/filter-chains")
assert.response(res).has.status(200)

body = assert.response(res).has.jsonbody()
assert.equals(1, #body.data, "unexpected number of filter chain entities")
assert.same(chain, body.data[1])

assert.response(
admin:post("/wasm/filter-chains", json {
name = "test-2",
filters = { { name = "test" } },
})
).has.status(201)

res = admin:get("/wasm/filter-chains")
assert.response(res).has.status(200)

body = assert.response(res).has.jsonbody()
assert.equals(2, #body.data, "unexpected number of filter chain entities")
assert.equals("test-2", body.data[2].name)
end)
end)

unsupported("PATCH", "/wasm/filter-chains")
unsupported("PUT", "/wasm/filter-chains")
unsupported("DELETE", "/wasm/filter-chains")
end)

describe("/wasm/filter-chains/:chain", function()
describe("GET", function()
local id, name, chain

lazy_setup(function()
reset_db()

id = utils.uuid()
name = "test"
local res = admin:post("/wasm/filter-chains", json {
id = id,
name = name,
filters = { { name = "test" } },
})

assert.response(res).has.status(201)
chain = assert.response(res).has.jsonbody()
end)

lazy_teardown(reset_db)

it("fetches a filter chain by ID", function()
local res = admin:get("/wasm/filter-chains/" .. id)
assert.response(res).has.status(200)
local got = assert.response(res).has.jsonbody()
assert.same(chain, got)
end)

it("fetches a filter chain by name", function()
local res = admin:get("/wasm/filter-chains/" .. name)
assert.response(res).has.status(200)
local got = assert.response(res).has.jsonbody()
assert.same(chain, got)
end)

it("returns 404 if not found", function()
assert.response(
admin:get("/wasm/filter-chains/" .. "i-do-not-exist")
).has.status(404)

assert.response(
admin:get("/wasm/filter-chains/" .. utils.uuid())
).has.status(404)
end)
end)

describe("PATCH", function()
local id, name, chain

lazy_setup(function()
reset_db()

id = utils.uuid()
name = "test"
local res = admin:post("/wasm/filter-chains", json {
id = id,
name = name,
filters = { { name = "test" } },
})

assert.response(res).has.status(201)
chain = assert.response(res).has.jsonbody()
end)

lazy_teardown(reset_db)

it("updates a filter chain in-place", function()
assert.equals(ngx.null, chain.tags)
assert.is_true(chain.enabled)
local updated = chain.updated_at

local res = admin:patch("/wasm/filter-chains/" .. id, json {
tags = { "foo", "bar" },
enabled = false,
filters = {
{ name = "filter-1" },
{ name = "filter-2" },
},
})

assert.response(res).has.status(200)
local patched = assert.response(res).has.jsonbody()

assert.same({ "foo", "bar" }, patched.tags)
assert.is_false(patched.enabled)
assert.equals(2, #patched.filters)
assert.equals("filter-1", patched.filters[1].name)
assert.equals("filter-2", patched.filters[2].name)
end)
end)

describe("DELETE", function()
lazy_setup(reset_db)
lazy_teardown(reset_db)

local id, name
before_each(function()
id = utils.uuid()
name = "test"

assert.response(admin:post("/wasm/filter-chains", json {
id = id,
name = name,
filters = { { name = "test" } },
})).has.status(201)

assert.response(
admin:get("/wasm/filter-chains/" .. id)
).has.status(200)
end)


it("removes a filter chain by ID", function()
local res = admin:delete("/wasm/filter-chains/" .. id)
assert.response(res).has.status(204)

assert.response(
admin:get("/wasm/filter-chains/" .. id)
).has.status(404)

assert.response(
admin:get("/wasm/filter-chains/" .. name)
).has.status(404)
end)

it("removes a filter chain by name", function()
local res = admin:delete("/wasm/filter-chains/" .. name)
assert.response(res).has.status(204)

assert.response(
admin:get("/wasm/filter-chains/" .. id)
).has.status(404)

assert.response(
admin:get("/wasm/filter-chains/" .. name)
).has.status(404)
end)

end)

unsupported("POST", "/wasm/filter-chains/" .. utils.uuid())
end)
end)

end -- each strategy
Loading

0 comments on commit 61c8671

Please sign in to comment.