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

[feature] RESTful Admin API #257

Merged
merged 4 commits into from
May 28, 2015
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
19 changes: 19 additions & 0 deletions database/migrations/cassandra/2015-05-22-235608_plugins_fix.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
local Migration = {
name = "2015-05-22-235608_plugins_fix",

up = function(options)
return [[
CREATE INDEX IF NOT EXISTS ON keyauth_credentials(consumer_id);
CREATE INDEX IF NOT EXISTS ON basicauth_credentials(consumer_id);
]]
end,

down = function(options)
return [[
DROP INDEX keyauth_credentials_consumer_id_idx;
DROP INDEX basicauth_credentials_consumer_id_idx;
]]
end
}

return Migration
3 changes: 2 additions & 1 deletion kong-0.3.0-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ build = {
["kong.plugins.ssl.schema"] = "kong/plugins/ssl/schema.lua",

["kong.api.app"] = "kong/api/app.lua",
["kong.api.crud_helpers"] = "kong/api/crud_helpers.lua",
["kong.api.routes.kong"] = "kong/api/routes/kong.lua",
["kong.api.routes.apis"] = "kong/api/routes/apis.lua",
["kong.api.routes.consumers"] = "kong/api/routes/consumers.lua",
["kong.api.routes.plugins_configurations"] = "kong/api/routes/plugins_configurations.lua",
["kong.api.routes.base_controller"] = "kong/api/routes/base_controller.lua"
},
install = {
conf = { "kong.yml" },
Expand Down
158 changes: 100 additions & 58 deletions kong/api/app.lua
Original file line number Diff line number Diff line change
@@ -1,72 +1,92 @@
local lapis = require "lapis"
local utils = require "kong.tools.utils"
local stringy = require "stringy"
local responses = require "kong.tools.responses"
local constants = require "kong.constants"

local Apis = require "kong.api.routes.apis"
local Consumers = require "kong.api.routes.consumers"
local PluginsConfigurations = require "kong.api.routes.plugins_configurations"

app = lapis.Application()

-- Huge hack to support PATCH methods.
-- This is a copy/pasted and adapted method from Lapis application.lua
-- It registers a method on `app.patch` listening for PATCH requests.
function app:patch(route_name, path, handler)
local lapis_application = require "lapis.application"
if handler == nil then
handler = path
path = route_name
route_name = nil
end
self.responders = self.responders or {}
local existing = self.responders[route_name or path]
local tbl = { ["PATCH"] = handler }
if existing then
setmetatable(tbl, {
__index = function(self, key)
if key:match("%u") then
return existing
end
local app_helpers = require "lapis.application"
local app = lapis.Application()

-- Put nested keys in objects:
-- Normalize dotted keys in objects.
-- Example: {["key.value.sub"]=1234} becomes {key = {value = {sub=1234}}
-- @param `obj` Object to normalize
-- @return `normalized_object`
local function normalize_nested_params(obj)
local normalized_obj = {} -- create a copy to not modify obj while it is in a loop.

for k, v in pairs(obj) do
if type(v) == "table" then
-- normalize arrays since Lapis parses ?key[1]=foo as {["1"]="foo"} instead of {"foo"}
if utils.is_array(v) then
local arr = {}
for _, arr_v in pairs(v) do table.insert(arr, arr_v) end
v = arr
else
v = normalize_nested_params(v) -- recursive call on other table values
end
end

-- normalize sub-keys with dot notation
local keys = stringy.split(k, ".")
if #keys > 1 then -- we have a key containing a dot
local current_level = keys[1] -- let's create an empty object for the first level
if normalized_obj[current_level] == nil then
normalized_obj[current_level] = {}
end
})
table.remove(keys, 1) -- remove the first level
normalized_obj[k] = nil -- remove it from the object
if #keys > 0 then -- if we still have some keys, then there are more levels of nestinf
normalized_obj[current_level][table.concat(keys, ".")] = v
normalized_obj[current_level] = normalize_nested_params(normalized_obj[current_level])
else
normalized_obj[current_level] = v -- latest level of nesting, attaching the value
end
else
normalized_obj[k] = v -- nothing special with that key, simply attaching the value
end
end
local responder = lapis_application.respond_to(tbl)
self.responders[route_name or path] = responder
return self:match(route_name, path, responder)
end

local function get_hostname()
local f = io.popen ("/bin/hostname")
local hostname = f:read("*a") or ""
f:close()
hostname = string.gsub(hostname, "\n$", "")
return hostname
return normalized_obj
end

app:get("/", function(self)
local db_plugins, err = dao.plugins_configurations:find_distinct()
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
local function default_on_error(self)
local err = self.errors[1]
if type(err) == "table" then
if err.database then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err.message)
elseif err.unique then
return responses.send_HTTP_CONFLICT(err.message)
elseif err.foreign then
return responses.send_HTTP_NOT_FOUND(err.message)
elseif err.invalid_type and err.message.id then
return responses.send_HTTP_BAD_REQUEST(err.message)
else
return responses.send_HTTP_BAD_REQUEST(err.message)
end
end
end

return responses.send_HTTP_OK({
tagline = "Welcome to Kong",
version = constants.VERSION,
hostname = get_hostname(),
plugins = {
available_on_server = configuration.plugins_available,
enabled_in_cluster = db_plugins
},
lua_version = jit and jit.version or _VERSION
})
end)
local function parse_params(fn)
return app_helpers.json_params(function(self, ...)
local content_type = self.req.headers["content-type"]
if content_type and string.find(content_type:lower(), "application/json", nil, true) then
if not self.json then
return responses.send_HTTP_BAD_REQUEST("Cannot parse JSON body")
end
end
self.params = normalize_nested_params(self.params)
return fn(self, ...)
end)
end

app.parse_params = parse_params

app.default_route = function(self)
local path = self.req.parsed_url.path:match("^(.*)/$")

if path and self.app.router:resolve(path, self) then
return
elseif self.app.router:resolve(self.req.parsed_url.path.."/", self) then
return
end

return self.app.handle_404(self)
Expand Down Expand Up @@ -96,18 +116,40 @@ app.handle_error = function(self, err, trace)
end
end

-- Load controllers
Apis()
Consumers()
PluginsConfigurations()
local handler_helpers = {
responses = responses,
yield_error = app_helpers.yield_error
}

local function attach_routes(routes)
for route_path, methods in pairs(routes) do
if not methods.on_error then
methods.on_error = default_on_error
end

for k, v in pairs(methods) do
local method = function(self)
return v(self, dao, handler_helpers)
end
methods[k] = parse_params(method)
end

app:match(route_path, route_path, app_helpers.respond_to(methods))
end
end

for _, v in ipairs({"kong", "apis", "consumers", "plugins_configurations"}) do
local routes = require("kong.api.routes."..v)
attach_routes(routes)
end

-- Loading plugins routes
if configuration and configuration.plugins_available then
for _, v in ipairs(configuration.plugins_available) do
local loaded, mod = utils.load_module_if_exists("kong.plugins."..v..".api")
if loaded then
ngx.log(ngx.DEBUG, "Loading API endpoints for plugin: "..v)
mod()
attach_routes(mod)
else
ngx.log(ngx.DEBUG, "No API endpoints loaded for plugin: "..v)
end
Expand Down
127 changes: 127 additions & 0 deletions kong/api/crud_helpers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
local responses = require "kong.tools.responses"
local validations = require "kong.dao.schemas_validation"
local app_helpers = require "lapis.application"

local _M = {}

function _M.find_api_by_name_or_id(self, dao_factory, helpers)
local fetch_keys = {
[validations.is_valid_uuid(self.params.name_or_id) and "id" or "name"] = self.params.name_or_id
}
self.params.name_or_id = nil

-- TODO: make the base_dao more flexible so we can query find_one with key/values
-- https://github.com/Mashape/kong/issues/103
local data, err = dao_factory.apis:find_by_keys(fetch_keys)
if err then
return helpers.yield_error(err)
end

self.api = data[1]
if not self.api then
return helpers.responses.send_HTTP_NOT_FOUND()
end
end

function _M.find_consumer_by_username_or_id(self, dao_factory, helpers)
local fetch_keys = {
[validations.is_valid_uuid(self.params.username_or_id) and "id" or "username"] = self.params.username_or_id
}
self.params.username_or_id = nil

local data, err = dao_factory.consumers:find_by_keys(fetch_keys)
if err then
return helpers.yield_error(err)
end

self.consumer = data[1]
if not self.consumer then
return helpers.responses.send_HTTP_NOT_FOUND()
end
end

function _M.paginated_set(self, dao_collection)
local size = self.params.size and tonumber(self.params.size) or 100
local offset = self.params.offset and ngx.decode_base64(self.params.offset) or nil

self.params.size = nil
self.params.offset = nil

local data, err = dao_collection:find_by_keys(self.params, size, offset)
if err then
return app_helpers.yield_error(err)
end

local next_url
if data.next_page then
next_url = self:build_url(self.req.parsed_url.path, {
port = self.req.parsed_url.port,
query = ngx.encode_args({
offset = ngx.encode_base64(data.next_page),
size = size
})
})
data.next_page = nil
end

-- This check is required otherwise the response is going to be a
-- JSON Object and not a JSON array. The reason is because an empty Lua array `{}`
-- will not be translated as an empty array by cjson, but as an empty object.
local result = #data == 0 and "{\"data\":[]}" or {data=data, ["next"]=next_url}

return responses.send_HTTP_OK(result, type(result) ~= "table")
end

function _M.put(params, dao_collection)
local new_entity, err
if params.id then
new_entity, err = dao_collection:update(params)
if not err and new_entity then
return responses.send_HTTP_OK(new_entity)
elseif not new_entity then
return responses.send_HTTP_NOT_FOUND()
end
else
new_entity, err = dao_collection:insert(params)
if not err then
return responses.send_HTTP_CREATED(new_entity)
end
end

if err then
return app_helpers.yield_error(err)
end
end

function _M.post(params, dao_collection)
local data, err = dao_collection:insert(params)
if err then
return app_helpers.yield_error(err)
else
return responses.send_HTTP_CREATED(data)
end
end

function _M.patch(params, dao_collection)
local new_entity, err = dao_collection:update(params)
if err then
return app_helpers.yield_error(err)
else
return responses.send_HTTP_OK(new_entity)
end
end

function _M.delete(entity_id, dao_collection)
local ok, err = dao_collection:delete(entity_id)
if not ok then
if err then
return app_helpers.yield_error(err)
else
return responses.send_HTTP_NOT_FOUND()
end
else
return responses.send_HTTP_NO_CONTENT()
end
end

return _M
Loading