Skip to content

Commit

Permalink
Merge pull request #257 from Mashape/feature/restful-api
Browse files Browse the repository at this point in the history
[feature] RESTful Admin API
  • Loading branch information
thibaultcha committed May 28, 2015
2 parents ec28b4e + 4605e51 commit a978448
Show file tree
Hide file tree
Showing 28 changed files with 1,611 additions and 335 deletions.
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

0 comments on commit a978448

Please sign in to comment.