Separate out mgmt components into their own files
diff --git a/api-gateway-config/conf.d/management_apis.conf b/api-gateway-config/conf.d/management_apis.conf
index 3e31390..3561720 100644
--- a/api-gateway-config/conf.d/management_apis.conf
+++ b/api-gateway-config/conf.d/management_apis.conf
@@ -35,16 +35,16 @@
location /v1/apis {
access_by_lua_block {
- local mgmt = require("management")
+ local apis = require("management/apis")
local requestMethod = ngx.req.get_method()
if requestMethod == "GET" then
- mgmt.getAPIs()
+ apis.getAPIs()
elseif requestMethod == "PUT" then
- mgmt.addAPI()
+ apis.addAPI()
elseif requestMethod == "POST" then
- mgmt.addAPI()
+ apis.addAPI()
elseif requestMethod == "DELETE" then
- mgmt.deleteAPI()
+ apis.deleteAPI()
else
ngx.status = 400
ngx.say("Invalid verb")
@@ -54,16 +54,16 @@
location /v1/tenants {
access_by_lua_block {
- local mgmt = require("management")
+ local tenants = require("management/tenants")
local requestMethod = ngx.req.get_method()
if requestMethod == "GET" then
- mgmt.getTenants()
+ tenants.getTenants()
elseif requestMethod == "PUT" then
- mgmt.addTenant()
+ tenants.addTenant()
elseif requestMethod == "POST" then
- mgmt.addTenant()
+ tenants.addTenant()
elseif requestMethod == "DELETE" then
- mgmt.deleteTenant()
+ tenants.deleteTenant()
else
ngx.status = 400
ngx.say("Invalid verb")
@@ -71,14 +71,14 @@
}
}
- location /subscriptions {
+ location /v1/subscriptions {
access_by_lua_block {
- local mgmt = require("management")
+ local subscriptions = require("management/subscriptions")
local requestMethod = ngx.req.get_method()
if requestMethod == "PUT" then
- mgmt.addSubscription()
+ subscriptions.addSubscription()
elseif requestMethod == "DELETE" then
- mgmt.deleteSubscription()
+ subscriptions.deleteSubscription()
else
ngx.status = 400
ngx.say("Invalid verb")
@@ -86,24 +86,19 @@
}
}
- location /v1/sync {
- access_by_lua_block {
- local mgmt = require("management")
- local requestMethod = ngx.req.get_method()
- if requestMethod == "GET" then
- mgmt.sync()
- else
- ngx.say("Invalid verb")
- end
- }
- }
-
location /v1/subscribe {
access_by_lua_block {
- local mgmt = require("management")
local requestMethod = ngx.req.get_method()
if requestMethod == "GET" then
- mgmt.subscribe()
+ local redis = require "lib/redis"
+ local logger = require "lib/logger"
+ local utils = require "lib/utils"
+ local REDIS_HOST = os.getenv("REDIS_HOST")
+ local REDIS_PORT = os.getenv("REDIS_PORT")
+ local REDIS_PASS = os.getenv("REDIS_PASS")
+ redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS)
+ logger.info(utils.concatStrings({"Connected to redis at ", REDIS_HOST, ":", REDIS_PORT}))
+ while true do end
else
ngx.say("Invalid verb")
end
@@ -112,10 +107,10 @@
location /v1/health-check {
access_by_lua_block {
- local mgmt = require("management")
local requestMethod = ngx.req.get_method()
if requestMethod == "GET" then
- mgmt.healthCheck()
+ local request = require "lib/request"
+ request.success(200, "Status: Gateway ready.")
else
ngx.say("Invalid verb")
end
diff --git a/api-gateway-config/scripts/lua/lib/filemgmt.lua b/api-gateway-config/scripts/lua/lib/filemgmt.lua
deleted file mode 100644
index f1a6e1c..0000000
--- a/api-gateway-config/scripts/lua/lib/filemgmt.lua
+++ /dev/null
@@ -1,90 +0,0 @@
--- Copyright (c) 2016 IBM. All rights reserved.
---
--- Permission is hereby granted, free of charge, to any person obtaining a
--- copy of this software and associated documentation files (the "Software"),
--- to deal in the Software without restriction, including without limitation
--- the rights to use, copy, modify, merge, publish, distribute, sublicense,
--- and/or sell copies of the Software, and to permit persons to whom the
--- Software is furnished to do so, subject to the following conditions:
---
--- The above copyright notice and this permission notice shall be included in
--- all copies or substantial portions of the Software.
---
--- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
--- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
--- DEALINGS IN THE SOFTWARE.
-
---- @module filemgmt
--- Creates the Nginx Conf files
--- @author Alex Song (songs), David Green (greend)
-
-local utils = require "lib/utils"
-local cjson = require "cjson"
-local request = require "lib/request"
-
-local _M = {}
-
---- Create/overwrite Nginx Conf file for given resource
--- @param baseConfDir the base directory for storing conf files for managed resources
--- @param tenant the namespace for the resource
--- @param gatewayPath the gateway path of the resource
--- @param resourceObj object containing different operations/policies for the resource
--- @return fileLocation location of created/updated conf file
-function _M.createResourceConf(baseConfDir, tenant, gatewayPath, resourceObj)
- local decoded = cjson.decode(resourceObj)
- resourceObj = utils.serializeTable(decoded)
- local prefix = utils.concatStrings({
- "\tinclude /etc/api-gateway/conf.d/commons/common-headers.conf;\n",
- "\tset $upstream https://172.17.0.1;\n",
- "\tset $tenant ", tenant, ";\n",
- "\tset $backendUrl '';\n",
- "\tset $gatewayPath '", ngx.unescape_uri(gatewayPath), "';\n"
- })
- if decoded.apiId ~= nil then
- prefix = utils.concatStrings({prefix, "\tset $apiId ", decoded.apiId, ";\n"})
- end
- -- Add CORS headers
- prefix = utils.concatStrings({prefix, "\tadd_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS';\n"})
- -- Set resource headers and mapping by calling routing.processCall()
- local outgoingResource = utils.concatStrings({
- "\taccess_by_lua_block {\n",
- "\t\tlocal routing = require \"routing\"\n",
- "\t\trouting.processCall(", resourceObj, ")\n",
- "\t}\n",
- "\tproxy_pass $upstream;\n"
- })
- -- Add to endpoint conf file
- os.execute(utils.concatStrings({"mkdir -p ", baseConfDir, tenant}))
- local fileLocation = utils.concatStrings({baseConfDir, tenant, "/", gatewayPath, ".conf"})
- local file, err = io.open(fileLocation, "w")
- if not file then
- request.err(500, utils.concatStrings({"Error adding to endpoint conf file: ", err}))
- end
- local updatedPath = ngx.unescape_uri(gatewayPath):gsub("%{(%w*)%}", utils.convertTemplatedPathParam)
- local location = utils.concatStrings({
- "location ~ ^/api/", tenant, "/", updatedPath, "(\\b) {\n",
- prefix,
- outgoingResource,
- "}\n"
- })
- file:write(location)
- file:close()
- return fileLocation
-end
-
---- Delete Ngx conf file for given resource
--- @param baseConfDir the base directory for storing conf files for managed resources
--- @param tenant the namespace for the resource
--- @param gatewayPath the gateway path of the resource
--- @return fileLocation location of deleted conf file
-function _M.deleteResourceConf(baseConfDir, tenant, gatewayPath)
- local fileLocation = utils.concatStrings({baseConfDir, tenant, "/", gatewayPath, ".conf"})
- os.execute(utils.concatStrings({"rm -f ", fileLocation}))
- return fileLocation
-end
-
-return _M
diff --git a/api-gateway-config/scripts/lua/lib/redis.lua b/api-gateway-config/scripts/lua/lib/redis.lua
index 37491ca..8a4b47c 100644
--- a/api-gateway-config/scripts/lua/lib/redis.lua
+++ b/api-gateway-config/scripts/lua/lib/redis.lua
@@ -18,9 +18,8 @@
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.
---- @module
---
--- @author Alex Song (songs)
+--- @module redis
+-- Module that the gateway uses to interact with redis
local cjson = require "cjson"
local utils = require "lib/utils"
diff --git a/api-gateway-config/scripts/lua/lib/utils.lua b/api-gateway-config/scripts/lua/lib/utils.lua
index d757c39..e50f56b 100644
--- a/api-gateway-config/scripts/lua/lib/utils.lua
+++ b/api-gateway-config/scripts/lua/lib/utils.lua
@@ -19,8 +19,7 @@
-- DEALINGS IN THE SOFTWARE.
--- @module utils
--- Holds the common supporting functions in one file to be referenced else where
--- @author Alex Song (songs), Cody Walker (cmwalker), David Green (greend)
+-- Holds the common supporting functions in one file to be referenced elsewhere
local _Utils = {}
@@ -28,7 +27,7 @@
-- strings together with "..", which creates a new string every time
-- @param list List of strings to concatenate
-- @return concatenated string
-function concatStrings(list)
+function _Utils.concatStrings(list)
local t = {}
for k,v in ipairs(list) do
t[#t+1] = tostring(v)
@@ -41,7 +40,7 @@
-- Useful for saving a lua table to a file, as if not serialized it will save as "Table x35252"
-- @param t The lua table
-- @return String representing the serialized lua table
-function serializeTable(t)
+function _Utils.serializeTable(t)
local first = true
local tt = { '{' }
for k, v in pairs(t) do
@@ -69,13 +68,13 @@
-- at time of being called by the user
-- @param m where m is the string "{pathParam}"
-- @return concatenated string of (?<path_pathParam>(\\w+))
-function convertTemplatedPathParam(m)
+function _Utils.convertTemplatedPathParam(m)
local x = m:gsub("{", ""):gsub("}", "")
return concatStrings({"(?<path_" , x , ">([a-zA-Z0-9\\-\\s\\_\\%]*))"})
end
--- Generate random uuid
-function uuid()
+function _Utils.uuid()
local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
math.randomseed(os.clock())
return string.gsub(template, '[xy]', function (c)
@@ -87,7 +86,7 @@
--- Check if element exists in table as value
-- @param table table to check
-- @param element element to check in table
-function tableContains(table, element)
+function _Utils.tableContains(table, element)
for i, value in pairs(table) do
if value == element then
return true
@@ -96,11 +95,5 @@
return false
end
-_Utils.concatStrings = concatStrings
-_Utils.serializeTable = serializeTable
-_Utils.convertTemplatedPathParam = convertTemplatedPathParam
-_Utils.uuid = uuid
-_Utils.tableContains = tableContains
-
return _Utils
diff --git a/api-gateway-config/scripts/lua/management.lua b/api-gateway-config/scripts/lua/management.lua
deleted file mode 100644
index 8734210..0000000
--- a/api-gateway-config/scripts/lua/management.lua
+++ /dev/null
@@ -1,666 +0,0 @@
--- Copyright (c) 2016 IBM. All rights reserved.
---
--- Permission is hereby granted, free of charge, to any person obtaining a
--- copy of this software and associated documentation files (the "Software"),
--- to deal in the Software without restriction, including without limitation
--- the rights to use, copy, modify, merge, publish, distribute, sublicense,
--- and/or sell copies of the Software, and to permit persons to whom the
--- Software is furnished to do so, subject to the following conditions:
---
--- The above copyright notice and this permission notice shall be included in
--- all copies or substantial portions of the Software.
---
--- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
--- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
--- DEALINGS IN THE SOFTWARE.
-
---- @module management
--- Defines and exposes a lightweight API management to create and remove resources in the running API Gateway
--- @author Alex Song (songs)
-
-local cjson = require "cjson"
-local redis = require "lib/redis"
-local utils = require "lib/utils"
-local logger = require "lib/logger"
-local request = require "lib/request"
-local MANAGEDURL_HOST = os.getenv("PUBLIC_MANAGEDURL_HOST")
-MANAGEDURL_HOST = (MANAGEDURL_HOST ~= nil and MANAGEDURL_HOST ~= '') and MANAGEDURL_HOST or "0.0.0.0"
-local MANAGEDURL_PORT = os.getenv("PUBLIC_MANAGEDURL_PORT")
-MANAGEDURL_PORT = (MANAGEDURL_PORT ~= nil and MANAGEDURL_PORT ~= '') and MANAGEDURL_PORT or "8080"
-local REDIS_HOST = os.getenv("REDIS_HOST")
-local REDIS_PORT = os.getenv("REDIS_PORT")
-local REDIS_PASS = os.getenv("REDIS_PASS")
-local REDIS_FIELD = "resources"
-local BASE_CONF_DIR = "/etc/api-gateway/managed_confs/"
-
-local _M = {}
-
---------------------------
----------- APIs ----------
---------------------------
-
---- Add an api to the Gateway
--- PUT /v1/apis
--- body:
--- {
--- "name": *(String) name of API
--- "basePath": *(String) base path for api
--- "tenantId": *(String) tenant id
--- "resources": *(String) resources to add
--- }
-function _M.addAPI()
- -- Open connection to redis or use one from connection pool
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- -- Check for api id from uri and use existingAPI if it already exists in redis
- local uri = string.gsub(ngx.var.request_uri, "?.*", "")
- local existingAPI = checkURIForExisting(red, uri, "api")
- -- Read in the PUT JSON Body
- ngx.req.read_body()
- local args = ngx.req.get_body_data()
- if not args then
- request.err(400, "Missing request body")
- end
- -- Convert json into Lua table
- local decoded = cjson.decode(args)
- -- Check for api id in JSON body
- if existingAPI == nil and decoded.id ~= nil then
- existingAPI = redis.getAPI(red, decoded.id)
- if existingAPI == nil then
- request.err(404, utils.concatStrings({"Unknown API id ", decoded.id}))
- end
- end
- -- Error checking
- local fields = {"name", "basePath", "tenantId", "resources"}
- for k, v in pairs(fields) do
- local res, err = isValid(red, v, decoded[v])
- if res == false then
- request.err(err.statusCode, err.message)
- end
- end
- -- Format basePath
- local basePath = decoded.basePath:sub(1,1) == '/' and decoded.basePath:sub(2) or decoded.basePath
- basePath = basePath:sub(-1) == '/' and basePath:sub(1, -2) or basePath
- -- Create managedUrl object
- local uuid = existingAPI ~= nil and existingAPI.id or utils.uuid()
- local managedUrl = utils.concatStrings({"http://", MANAGEDURL_HOST, ":", MANAGEDURL_PORT, "/api/", decoded.tenantId})
- if basePath:sub(1,1) ~= '' then
- managedUrl = utils.concatStrings({managedUrl, "/", basePath})
- end
- local managedUrlObj = {
- id = uuid,
- name = decoded.name,
- basePath = utils.concatStrings({"/", basePath}),
- tenantId = decoded.tenantId,
- resources = decoded.resources,
- managedUrl = managedUrl
- }
- -- Add API object to redis
- managedUrlObj = redis.addAPI(red, uuid, managedUrlObj, existingAPI)
- -- Add resources to redis
- for path, resource in pairs(decoded.resources) do
- local gatewayPath = utils.concatStrings({basePath, path})
- gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
- addResource(red, resource, gatewayPath, decoded.tenantId)
- end
- redis.close(red)
- -- Return managed url object
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, managedUrlObj)
-end
-
---- Check JSON body fields for errors
--- @param red Redis client instance
--- @param field name of field
--- @param object field object
-function isValid(red, field, object)
- -- Check that field exists in body
- if not object then
- return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", field, "' in request body."}) }
- end
- -- Additional check for basePath
- if field == "basePath" then
- local basePath = object
- if string.match(basePath, "'") then
- return false, { statusCode = 400, message = "basePath contains illegal character \"'\"." }
- end
- end
- -- Additional check for tenantId
- if field == "tenantId" then
- local tenant = redis.getTenant(red, object)
- if tenant == nil then
- return false, { statusCode = 404, message = utils.concatStrings({"Unknown tenant id ", object }) }
- end
- end
- if field == "resources" then
- local res, err = checkResources(object)
- if res ~= nil and res == false then
- return res, err
- end
- end
- -- All error checks passed
- return true
-end
-
---- Error checking for resources
--- @param resources resources object
-function checkResources(resources)
- if next(resources) == nil then
- return false, { statusCode = 400, message = "Empty resources object." }
- end
- for path, resource in pairs(resources) do
- -- Check resource path for illegal characters
- if string.match(path, "'") then
- return false, { statusCode = 400, message = "resource path contains illegal character \"'\"." }
- end
- -- Check that resource path begins with slash
- if path:sub(1,1) ~= '/' then
- return false, { statusCode = 400, message = "Resource path must begin with '/'." }
- end
- -- Check operations object
- local res, err = checkOperations(resource.operations)
- if res ~= nil and res == false then
- return res, err
- end
- end
-end
-
---- Error checking for operations
--- @param operations operations object
-function checkOperations(operations)
- if not operations or next(operations) == nil then
- return false, { statusCode = 400, message = "Missing or empty field 'operations' or in resource path object." }
- end
- local allowedVerbs = {GET=true, POST=true, PUT=true, DELETE=true, PATCH=true, HEAD=true, OPTIONS=true}
- for verb, verbObj in pairs(operations) do
- if allowedVerbs[verb:upper()] == nil then
- return false, { statusCode = 400, message = utils.concatStrings({"Resource verb '", verb, "' not supported."}) }
- end
- -- Check required fields
- local requiredFields = {"backendMethod", "backendUrl"}
- for k, v in pairs(requiredFields) do
- if verbObj[v] == nil then
- return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", v, "' for '", verb, "' operation."}) }
- end
- if v == "backendMethod" then
- local backendMethod = verbObj[v]
- if allowedVerbs[backendMethod:upper()] == nil then
- return false, { statusCode = 400, message = utils.concatStrings({"backendMethod '", backendMethod, "' not supported."}) }
- end
- end
- end
- -- Check optional fields
- local res, err = checkOptionalPolicies(verbObj.policies, verbObj.security)
- if res ~= nil and res == false then
- return res, err
- end
- end
-end
-
---- Error checking for policies and security
--- @param policies policies object
--- @param security security object
-function checkOptionalPolicies(policies, security)
- if policies then
- for k, v in pairs(policies) do
- local validTypes = {reqMapping = true, rateLimit = true}
- if (v.type == nil or v.value == nil) then
- return false, { statusCode = 400, message = "Missing field in policy object. Need \"type\" and \"scope\"." }
- elseif validTypes[v.type] == nil then
- return false, { statusCode = 400, message = "Invalid type in policy object. Valid: \"reqMapping\", \"rateLimit\"" }
- end
- end
- end
- if security then
- local validScopes = {tenant=true, api=true, resource=true}
- if (security.type == nil or security.scope == nil) then
- return false, { statusCode = 400, message = "Missing field in security object. Need \"type\" and \"scope\"." }
- elseif validScopes[security.scope] == nil then
- return false, { statusCode = 400, message = "Invalid scope in security object. Valid: \"tenant\", \"api\", \"resource\"." }
- end
- end
-end
-
---- Helper function for adding a resource to redis and creating an nginx conf file
--- @param red
--- @param resource
--- @param gatewayPath
--- @param tenantId
-function addResource(red, resource, gatewayPath, tenantId)
- -- Create resource object and add to redis
- local redisKey = utils.concatStrings({"resources", ":", tenantId, ":", gatewayPath})
- local apiId
- local operations
- for k, v in pairs(resource) do
- if k == 'apiId' then
- apiId = v
- elseif k == 'operations' then
- operations = v
- end
- end
- local resourceObj = redis.generateResourceObj(operations, apiId)
- redis.createResource(red, redisKey, REDIS_FIELD, resourceObj)
-end
-
---- Get one or all APIs from the gateway
--- GET /v1/apis
-function _M.getAPIs()
- local uri = string.gsub(ngx.var.request_uri, "?.*", "")
- local id
- local index = 1
- local tenantQuery = false
- for word in string.gmatch(uri, '([^/]+)') do
- if index == 3 then
- id = word
- elseif index == 4 then
- if word == 'tenant' then
- tenantQuery = true
- else
- request.err(400, "Invalid request")
- end
- end
- index = index + 1
- end
- if id == nil then
- getAllAPIs()
- else
- if tenantQuery == false then
- getAPI(id)
- else
- getAPITenant(id)
- end
- end
-end
-
---- Get all APIs in redis
-function getAllAPIs()
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local res = redis.getAllAPIs(red)
- redis.close(red)
- local apiList = {}
- for k, v in pairs(res) do
- if k%2 == 0 then
- apiList[#apiList+1] = cjson.decode(v)
- end
- end
- apiList = cjson.encode(apiList)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, apiList)
-end
-
---- Get API by its id
--- @param id of API
-function getAPI(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local api = redis.getAPI(red, id)
- if api == nil then
- request.err(404, utils.concatStrings({"Unknown api id ", id}))
- end
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, cjson.encode(api))
-end
-
---- Get belongsTo relation tenant
--- @param id id of API
-function getAPITenant(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local api = redis.getAPI(red, id)
- if api == nil then
- request.err(404, utils.concatStrings({"Unknown api id ", id}))
- end
- local tenantId = api.tenantId
- local tenant = redis.getTenant(red, tenantId)
- if tenant == nil then
- request.err(404, utils.concatStrings({"Unknown tenant id ", tenantId}))
- end
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, cjson.encode(tenant))
-end
-
---- Delete API from gateway
--- DELETE /v1/apis/<id>
-function _M.deleteAPI()
- local uri = string.gsub(ngx.var.request_uri, "?.*", "")
- local index = 1
- local id
- for word in string.gmatch(uri, '([^/]+)') do
- if index == 3 then
- id = word
- end
- index = index + 1
- end
- if id == nil then
- request.err(400, "No id specified.")
- end
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local api = redis.getAPI(red, id)
- if api == nil then
- request.err(404, utils.concatStrings({"Unknown API id ", id}))
- end
- -- Delete API
- redis.deleteAPI(red, id)
- -- Delete all resources for the API
- local basePath = api.basePath:sub(2)
- for path, v in pairs(api.resources) do
- local gatewayPath = utils.concatStrings({basePath, path})
- gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
- deleteResource(red, gatewayPath, api.tenantId)
- end
- redis.close(red)
- request.success(200, {})
-end
-
---- Helper function for deleting resource in redis and appropriate conf files
--- @param red redis instance
--- @param gatewayPath path in gateway
--- @param tenantId tenant id
-function deleteResource(red, gatewayPath, tenantId)
- local redisKey = utils.concatStrings({"resources:", tenantId, ":", gatewayPath})
- redis.deleteResource(red, redisKey, REDIS_FIELD)
-end
-
------------------------------
----------- Tenants ----------
------------------------------
-
---- Add a tenant to the Gateway
--- PUT /v1/tenants
--- body:
--- {
--- "namespace": *(String) tenant namespace
--- "instance": *(String) tenant instance
--- }
-function _M.addTenant()
- -- Open connection to redis or use one from connection pool
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- -- Check for tenant id and use existingTenant if it already exists in redis
- local uri = string.gsub(ngx.var.request_uri, "?.*", "")
- local existingTenant = checkURIForExisting(red, uri, "tenant")
- -- Read in the PUT JSON Body
- ngx.req.read_body()
- local args = ngx.req.get_body_data()
- if not args then
- request.err(400, "Missing request body")
- end
- -- Convert json into Lua table
- local decoded = cjson.decode(args)
- -- Check for tenant id in JSON body
- if existingTenant == nil and decoded.id ~= nil then
- existingTenant = redis.getTenant(red, decoded.id)
- if existingTenant == nil then
- request.err(404, utils.concatStrings({"Unknown Tenant id ", decoded.id}))
- end
- end
- -- Error checking
- local fields = {"namespace", "instance"}
- for k, v in pairs(fields) do
- if not decoded[v] then
- request.err(400, utils.concatStrings({"Missing field '", v, "' in request body."}))
- end
- end
- -- Return tenant object
- local uuid = existingTenant ~= nil and existingTenant.id or utils.uuid()
- local tenantObj = {
- id = uuid,
- namespace = decoded.namespace,
- instance = decoded.instance
- }
- tenantObj = redis.addTenant(red, uuid, tenantObj)
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, tenantObj)
-end
-
---- Get one or all tenants from the gateway
--- GET /v1/tenants
-function _M.getTenants()
- local uri = string.gsub(ngx.var.request_uri, "?.*", "")
- local id
- local index = 1
- local apiQuery = false
- for word in string.gmatch(uri, '([^/]+)') do
- if index == 3 then
- id = word
- elseif index == 4 then
- if word:lower() == 'apis' then
- apiQuery = true
- else
- request.err(400, "Invalid request")
- end
- end
- index = index + 1
- end
- if id == nil then
- getAllTenants()
- else
- if apiQuery == false then
- getTenant(id)
- else
- getTenantAPIs(id)
- end
- end
-end
-
---- Get all tenants in redis
-function getAllTenants()
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local res = redis.getAllTenants(red)
- redis.close(red)
- local tenantList = {}
- for k, v in pairs(res) do
- if k%2 == 0 then
- tenantList[#tenantList+1] = cjson.decode(v)
- end
- end
- tenantList = cjson.encode(tenantList)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, tenantList)
-end
-
---- Get tenant by its id
--- @param id tenant id
-function getTenant(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local tenant = redis.getTenant(red, id)
- if tenant == nil then
- request.err(404, utils.concatStrings({"Unknown tenant id ", id }))
- end
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, cjson.encode(tenant))
-end
-
---- Get APIs associated with tenant
--- @param id tenant id
-function getTenantAPIs(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local res = redis.getAllAPIs(red)
- redis.close(red)
- local apiList = {}
- for k, v in pairs(res) do
- if k%2 == 0 then
- local decoded = cjson.decode(v)
- if decoded.tenantId == id then
- apiList[#apiList+1] = decoded
- end
- end
- end
- apiList = cjson.encode(apiList)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, apiList)
-end
-
---- Delete tenant from gateway
--- DELETE /v1/tenants/<id>
-function _M.deleteTenant()
- local uri = string.gsub(ngx.var.request_uri, "?.*", "")
- local index = 1
- local id
- for word in string.gmatch(uri, '([^/]+)') do
- if index == 3 then
- id = word
- end
- index = index + 1
- end
- if id == nil then
- request.err(400, "No id specified.")
- end
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 1000)
- redis.deleteTenant(red, id)
- redis.close(red)
- request.success(200, {})
-end
-
-----------------------------
-------- Health Check -------
-----------------------------
-
---- Check health of gateway
-function _M.healthCheck()
- redis.healthCheck()
-end
-
----------------------------
------- Subscriptions ------
----------------------------
-
---- Add an apikey/subscription to redis
--- PUT /subscriptions
--- Body:
--- {
--- key: *(String) key for tenant/api/resource
--- scope: *(String) tenant or api or resource
--- tenantId: *(String) tenant id
--- resource: (String) url-encoded resource path
--- apiId: (String) api id
--- }
-function _M.addSubscription()
- -- Validate body and create redisKey
- local redisKey = validateSubscriptionBody()
- -- Open connection to redis or use one from connection pool
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- redis.createSubscription(red, redisKey)
- -- Add current redis connection in the ngx_lua cosocket connection pool
- redis.close(red)
- request.success(200, "Subscription created.")
-end
-
---- Delete apikey/subscription from redis
--- DELETE /subscriptions
--- Body:
--- {
--- key: *(String) key for tenant/api/resource
--- scope: *(String) tenant or api or resource
--- tenantId: *(String) tenant id
--- resource: (String) url-encoded resource path
--- apiId: (String) api id
--- }
-function _M.deleteSubscription()
- -- Validate body and create redisKey
- local redisKey = validateSubscriptionBody()
- -- Initialize and connect to redis
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- -- Return if subscription doesn't exist
- redis.deleteSubscription(red, redisKey)
- -- Add current redis connection in the ngx_lua cosocket connection pool
- redis.close(red)
- request.success(200, "Subscription deleted.")
-end
-
---- Check the request JSON body for correct fields
--- @return redisKey subscription key for redis
-function validateSubscriptionBody()
- -- Read in the PUT JSON Body
- ngx.req.read_body()
- local args = ngx.req.get_body_data()
- if not args then
- request.err(400, "Missing request body.")
- end
- -- Convert json into Lua table
- local decoded = cjson.decode(args)
- -- Check required fields
- local requiredFieldList = {"key", "scope", "tenantId"}
- for i, field in ipairs(requiredFieldList) do
- if not decoded[field] then
- request.err(400, utils.concatStrings({"\"", field, "\" missing from request body."}))
- end
- end
- -- Check if we're using tenant or resource or api
- local resource = decoded.resource
- local apiId = decoded.apiId
- local redisKey
- local prefix = utils.concatStrings({"subscriptions:tenant:", decoded.tenantId})
- if decoded.scope == "tenant" then
- redisKey = prefix
- elseif decoded.scope == "resource" then
- if resource ~= nil then
- redisKey = utils.concatStrings({prefix, ":resource:", resource})
- else
- request.err(400, "\"resource\" missing from request body.")
- end
- elseif decoded.scope == "api" then
- if apiId ~= nil then
- redisKey = utils.concatStrings({prefix, ":api:", apiId})
- else
- request.err(400, "\"apiId\" missing from request body.")
- end
- else
- request.err(400, "Invalid scope")
- end
- redisKey = utils.concatStrings({redisKey, ":key:", decoded.key})
- return redisKey
-end
-
-
---- Check for api id from uri and use existingAPI if it already exists in redis
--- @param red Redis client instance
--- @param uri Uri of request. Eg. /v1/apis/{id}
--- @param type type to look for in redis. "api" or "tenant"
-function checkURIForExisting(red, uri, type)
- local id, existing
- local index = 1
- -- Check if id is in the uri
- for word in string.gmatch(uri, '([^/]+)') do
- if index == 3 then
- id = word
- end
- index = index + 1
- end
- -- Get object from redis
- if id ~= nil then
- if type == "api" then
- existing = redis.getAPI(red, id)
- if existing == nil then
- request.err(404, utils.concatStrings({"Unknown API id ", id}))
- end
- elseif type == "tenant" then
- existing = redis.getTenant(red, id)
- if existing == nil then
- request.err(404, utils.concatStrings({"Unknown Tenant id ", id}))
- end
- end
- end
- return existing
-end
-
---- Parse the request uri to get the redisKey, tenant, and gatewayPath
--- @param requestURI String containing the uri in the form of "/resources/<tenant>/<path>"
--- @return list containing redisKey, tenant, gatewayPath
-function parseRequestURI(requestURI)
- local list = {}
- for i in string.gmatch(requestURI, '([^/]+)') do
- list[#list + 1] = i
- end
- if not list[1] or not list[2] then
- request.err(400, "Request path should be \"/resources/<tenant>/<url-encoded-resource>\"")
- end
-
- return list --prefix, tenant, gatewayPath, apiKey
-end
-
-return _M
diff --git a/api-gateway-config/scripts/lua/management/apis.lua b/api-gateway-config/scripts/lua/management/apis.lua
new file mode 100644
index 0000000..484e9a5
--- /dev/null
+++ b/api-gateway-config/scripts/lua/management/apis.lua
@@ -0,0 +1,354 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module apis
+-- Management interface for apis for the gateway
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local resources = require "management/resources"
+
+local MANAGEDURL_HOST = os.getenv("PUBLIC_MANAGEDURL_HOST")
+MANAGEDURL_HOST = (MANAGEDURL_HOST ~= nil and MANAGEDURL_HOST ~= '') and MANAGEDURL_HOST or "0.0.0.0"
+local MANAGEDURL_PORT = os.getenv("PUBLIC_MANAGEDURL_PORT")
+MANAGEDURL_PORT = (MANAGEDURL_PORT ~= nil and MANAGEDURL_PORT ~= '') and MANAGEDURL_PORT or "8080"
+local REDIS_HOST = os.getenv("REDIS_HOST")
+local REDIS_PORT = os.getenv("REDIS_PORT")
+local REDIS_PASS = os.getenv("REDIS_PASS")
+
+local _M = {}
+
+--- Add an api to the Gateway
+-- PUT /v1/apis
+-- body:
+-- {
+-- "name": *(String) name of API
+-- "basePath": *(String) base path for api
+-- "tenantId": *(String) tenant id
+-- "resources": *(String) resources to add
+-- }
+function _M.addAPI()
+ -- Open connection to redis or use one from connection pool
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ -- Check for api id from uri and use existingAPI if it already exists in redis
+ local uri = string.gsub(ngx.var.request_uri, "?.*", "")
+ local existingAPI = checkURIForExistingAPI(red, uri)
+ -- Read in the PUT JSON Body
+ ngx.req.read_body()
+ local args = ngx.req.get_body_data()
+ if not args then
+ request.err(400, "Missing request body")
+ end
+ -- Convert json into Lua table
+ local decoded = cjson.decode(args)
+ -- Check for api id in JSON body
+ if existingAPI == nil and decoded.id ~= nil then
+ existingAPI = redis.getAPI(red, decoded.id)
+ if existingAPI == nil then
+ request.err(404, utils.concatStrings({"Unknown API id ", decoded.id}))
+ end
+ end
+ -- Error checking
+ local fields = {"name", "basePath", "tenantId", "resources"}
+ for k, v in pairs(fields) do
+ local res, err = isValid(red, v, decoded[v])
+ if res == false then
+ request.err(err.statusCode, err.message)
+ end
+ end
+ -- Format basePath
+ local basePath = decoded.basePath:sub(1,1) == '/' and decoded.basePath:sub(2) or decoded.basePath
+ basePath = basePath:sub(-1) == '/' and basePath:sub(1, -2) or basePath
+ -- Create managedUrl object
+ local uuid = existingAPI ~= nil and existingAPI.id or utils.uuid()
+ local managedUrl = utils.concatStrings({"http://", MANAGEDURL_HOST, ":", MANAGEDURL_PORT, "/api/", decoded.tenantId})
+ if basePath:sub(1,1) ~= '' then
+ managedUrl = utils.concatStrings({managedUrl, "/", basePath})
+ end
+ local managedUrlObj = {
+ id = uuid,
+ name = decoded.name,
+ basePath = utils.concatStrings({"/", basePath}),
+ tenantId = decoded.tenantId,
+ resources = decoded.resources,
+ managedUrl = managedUrl
+ }
+ -- Add API object to redis
+ managedUrlObj = redis.addAPI(red, uuid, managedUrlObj, existingAPI)
+ -- Add resources to redis
+ for path, resource in pairs(decoded.resources) do
+ local gatewayPath = utils.concatStrings({basePath, path})
+ gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
+ resources.addResource(red, resource, gatewayPath, decoded.tenantId)
+ end
+ redis.close(red)
+ -- Return managed url object
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, managedUrlObj)
+end
+
+--- Check for api id from uri and use existing API if it already exists in redis
+-- @param red Redis client instance
+-- @param uri Uri of request. Eg. /v1/apis/{id}
+function checkURIForExistingAPI(red, uri)
+ local id, existing
+ local index = 1
+ -- Check if id is in the uri
+ for word in string.gmatch(uri, '([^/]+)') do
+ if index == 3 then
+ id = word
+ end
+ index = index + 1
+ end
+ -- Get object from redis
+ if id ~= nil then
+ existing = redis.getAPI(red, id)
+ if existing == nil then
+ request.err(404, utils.concatStrings({"Unknown API id ", id}))
+ end
+ end
+ return existing
+end
+
+--- Check JSON body fields for errors
+-- @param red Redis client instance
+-- @param field name of field
+-- @param object field object
+function isValid(red, field, object)
+ -- Check that field exists in body
+ if not object then
+ return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", field, "' in request body."}) }
+ end
+ -- Additional check for basePath
+ if field == "basePath" then
+ local basePath = object
+ if string.match(basePath, "'") then
+ return false, { statusCode = 400, message = "basePath contains illegal character \"'\"." }
+ end
+ end
+ -- Additional check for tenantId
+ if field == "tenantId" then
+ local tenant = redis.getTenant(red, object)
+ if tenant == nil then
+ return false, { statusCode = 404, message = utils.concatStrings({"Unknown tenant id ", object }) }
+ end
+ end
+ if field == "resources" then
+ local res, err = checkResources(object)
+ if res ~= nil and res == false then
+ return res, err
+ end
+ end
+ -- All error checks passed
+ return true
+end
+
+--- Error checking for resources
+-- @param resources resources object
+function checkResources(resources)
+ if next(resources) == nil then
+ return false, { statusCode = 400, message = "Empty resources object." }
+ end
+ for path, resource in pairs(resources) do
+ -- Check resource path for illegal characters
+ if string.match(path, "'") then
+ return false, { statusCode = 400, message = "resource path contains illegal character \"'\"." }
+ end
+ -- Check that resource path begins with slash
+ if path:sub(1,1) ~= '/' then
+ return false, { statusCode = 400, message = "Resource path must begin with '/'." }
+ end
+ -- Check operations object
+ local res, err = checkOperations(resource.operations)
+ if res ~= nil and res == false then
+ return res, err
+ end
+ end
+end
+
+--- Error checking for operations
+-- @param operations operations object
+function checkOperations(operations)
+ if not operations or next(operations) == nil then
+ return false, { statusCode = 400, message = "Missing or empty field 'operations' or in resource path object." }
+ end
+ local allowedVerbs = {GET=true, POST=true, PUT=true, DELETE=true, PATCH=true, HEAD=true, OPTIONS=true}
+ for verb, verbObj in pairs(operations) do
+ if allowedVerbs[verb:upper()] == nil then
+ return false, { statusCode = 400, message = utils.concatStrings({"Resource verb '", verb, "' not supported."}) }
+ end
+ -- Check required fields
+ local requiredFields = {"backendMethod", "backendUrl"}
+ for k, v in pairs(requiredFields) do
+ if verbObj[v] == nil then
+ return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", v, "' for '", verb, "' operation."}) }
+ end
+ if v == "backendMethod" then
+ local backendMethod = verbObj[v]
+ if allowedVerbs[backendMethod:upper()] == nil then
+ return false, { statusCode = 400, message = utils.concatStrings({"backendMethod '", backendMethod, "' not supported."}) }
+ end
+ end
+ end
+ -- Check optional fields
+ local res, err = checkOptionalPolicies(verbObj.policies, verbObj.security)
+ if res ~= nil and res == false then
+ return res, err
+ end
+ end
+end
+
+--- Error checking for policies and security
+-- @param policies policies object
+-- @param security security object
+function checkOptionalPolicies(policies, security)
+ if policies then
+ for k, v in pairs(policies) do
+ local validTypes = {reqMapping = true, rateLimit = true}
+ if (v.type == nil or v.value == nil) then
+ return false, { statusCode = 400, message = "Missing field in policy object. Need \"type\" and \"scope\"." }
+ elseif validTypes[v.type] == nil then
+ return false, { statusCode = 400, message = "Invalid type in policy object. Valid: \"reqMapping\", \"rateLimit\"" }
+ end
+ end
+ end
+ if security then
+ local validScopes = {tenant=true, api=true, resource=true}
+ if (security.type == nil or security.scope == nil) then
+ return false, { statusCode = 400, message = "Missing field in security object. Need \"type\" and \"scope\"." }
+ elseif validScopes[security.scope] == nil then
+ return false, { statusCode = 400, message = "Invalid scope in security object. Valid: \"tenant\", \"api\", \"resource\"." }
+ end
+ end
+end
+
+--- Get one or all APIs from the gateway
+-- GET /v1/apis
+function _M.getAPIs()
+ local uri = string.gsub(ngx.var.request_uri, "?.*", "")
+ local id
+ local index = 1
+ local tenantQuery = false
+ for word in string.gmatch(uri, '([^/]+)') do
+ if index == 3 then
+ id = word
+ elseif index == 4 then
+ if word == 'tenant' then
+ tenantQuery = true
+ else
+ request.err(400, "Invalid request")
+ end
+ end
+ index = index + 1
+ end
+ if id == nil then
+ getAllAPIs()
+ else
+ if tenantQuery == false then
+ getAPI(id)
+ else
+ getAPITenant(id)
+ end
+ end
+end
+
+--- Get all APIs in redis
+function getAllAPIs()
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local res = redis.getAllAPIs(red)
+ redis.close(red)
+ local apiList = {}
+ for k, v in pairs(res) do
+ if k%2 == 0 then
+ apiList[#apiList+1] = cjson.decode(v)
+ end
+ end
+ apiList = cjson.encode(apiList)
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, apiList)
+end
+
+--- Get API by its id
+-- @param id of API
+function getAPI(id)
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local api = redis.getAPI(red, id)
+ if api == nil then
+ request.err(404, utils.concatStrings({"Unknown api id ", id}))
+ end
+ redis.close(red)
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, cjson.encode(api))
+end
+
+--- Get belongsTo relation tenant
+-- @param id id of API
+function getAPITenant(id)
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local api = redis.getAPI(red, id)
+ if api == nil then
+ request.err(404, utils.concatStrings({"Unknown api id ", id}))
+ end
+ local tenantId = api.tenantId
+ local tenant = redis.getTenant(red, tenantId)
+ if tenant == nil then
+ request.err(404, utils.concatStrings({"Unknown tenant id ", tenantId}))
+ end
+ redis.close(red)
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, cjson.encode(tenant))
+end
+
+--- Delete API from gateway
+-- DELETE /v1/apis/<id>
+function _M.deleteAPI()
+ local uri = string.gsub(ngx.var.request_uri, "?.*", "")
+ local index = 1
+ local id
+ for word in string.gmatch(uri, '([^/]+)') do
+ if index == 3 then
+ id = word
+ end
+ index = index + 1
+ end
+ if id == nil then
+ request.err(400, "No id specified.")
+ end
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local api = redis.getAPI(red, id)
+ if api == nil then
+ request.err(404, utils.concatStrings({"Unknown API id ", id}))
+ end
+ -- Delete API
+ redis.deleteAPI(red, id)
+ -- Delete all resources for the API
+ local basePath = api.basePath:sub(2)
+ for path, v in pairs(api.resources) do
+ local gatewayPath = utils.concatStrings({basePath, path})
+ gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
+ resources.deleteResource(red, gatewayPath, api.tenantId)
+ end
+ redis.close(red)
+ request.success(200, {})
+end
+
+return _M;
diff --git a/api-gateway-config/scripts/lua/management/resources.lua b/api-gateway-config/scripts/lua/management/resources.lua
new file mode 100644
index 0000000..6ec4095
--- /dev/null
+++ b/api-gateway-config/scripts/lua/management/resources.lua
@@ -0,0 +1,60 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module resources
+-- Management interface for resources for the gateway
+
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+
+local REDIS_FIELD = "resources"
+local _M = {}
+
+--- Helper function for adding a resource to redis and creating an nginx conf file
+-- @param red
+-- @param resource
+-- @param gatewayPath
+-- @param tenantId
+function _M.addResource(red, resource, gatewayPath, tenantId)
+ -- Create resource object and add to redis
+ local redisKey = utils.concatStrings({"resources", ":", tenantId, ":", gatewayPath})
+ local apiId
+ local operations
+ for k, v in pairs(resource) do
+ if k == 'apiId' then
+ apiId = v
+ elseif k == 'operations' then
+ operations = v
+ end
+ end
+ local resourceObj = redis.generateResourceObj(operations, apiId)
+ redis.createResource(red, redisKey, REDIS_FIELD, resourceObj)
+end
+
+--- Helper function for deleting resource in redis and appropriate conf files
+-- @param red redis instance
+-- @param gatewayPath path in gateway
+-- @param tenantId tenant id
+function _M.deleteResource(red, gatewayPath, tenantId)
+ local redisKey = utils.concatStrings({"resources:", tenantId, ":", gatewayPath})
+ redis.deleteResource(red, redisKey, REDIS_FIELD)
+end
+
+return _M
diff --git a/api-gateway-config/scripts/lua/management/subscriptions.lua b/api-gateway-config/scripts/lua/management/subscriptions.lua
new file mode 100644
index 0000000..7c8e80f
--- /dev/null
+++ b/api-gateway-config/scripts/lua/management/subscriptions.lua
@@ -0,0 +1,122 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module subscriptions
+-- Management interface for subscriptions for the gateway
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+
+local REDIS_HOST = os.getenv("REDIS_HOST")
+local REDIS_PORT = os.getenv("REDIS_PORT")
+local REDIS_PASS = os.getenv("REDIS_PASS")
+
+local _M = {}
+
+--- Add an apikey/subscription to redis
+-- PUT /subscriptions
+-- Body:
+-- {
+-- key: *(String) key for tenant/api/resource
+-- scope: *(String) tenant or api or resource
+-- tenantId: *(String) tenant id
+-- resource: (String) url-encoded resource path
+-- apiId: (String) api id
+-- }
+function _M.addSubscription()
+ -- Validate body and create redisKey
+ local redisKey = validateSubscriptionBody()
+ -- Open connection to redis or use one from connection pool
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ redis.createSubscription(red, redisKey)
+ -- Add current redis connection in the ngx_lua cosocket connection pool
+ redis.close(red)
+ request.success(200, "Subscription created.")
+end
+
+--- Delete apikey/subscription from redis
+-- DELETE /subscriptions
+-- Body:
+-- {
+-- key: *(String) key for tenant/api/resource
+-- scope: *(String) tenant or api or resource
+-- tenantId: *(String) tenant id
+-- resource: (String) url-encoded resource path
+-- apiId: (String) api id
+-- }
+function _M.deleteSubscription()
+ -- Validate body and create redisKey
+ local redisKey = validateSubscriptionBody()
+ -- Initialize and connect to redis
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ -- Return if subscription doesn't exist
+ redis.deleteSubscription(red, redisKey)
+ -- Add current redis connection in the ngx_lua cosocket connection pool
+ redis.close(red)
+ request.success(200, "Subscription deleted.")
+end
+
+--- Check the request JSON body for correct fields
+-- @return redisKey subscription key for redis
+function validateSubscriptionBody()
+ -- Read in the PUT JSON Body
+ ngx.req.read_body()
+ local args = ngx.req.get_body_data()
+ if not args then
+ request.err(400, "Missing request body.")
+ end
+ -- Convert json into Lua table
+ local decoded = cjson.decode(args)
+ -- Check required fields
+ local requiredFieldList = {"key", "scope", "tenantId"}
+ for i, field in ipairs(requiredFieldList) do
+ if not decoded[field] then
+ request.err(400, utils.concatStrings({"\"", field, "\" missing from request body."}))
+ end
+ end
+ -- Check if we're using tenant or resource or api
+ local resource = decoded.resource
+ local apiId = decoded.apiId
+ local redisKey
+ local prefix = utils.concatStrings({"subscriptions:tenant:", decoded.tenantId})
+ if decoded.scope == "tenant" then
+ redisKey = prefix
+ elseif decoded.scope == "resource" then
+ if resource ~= nil then
+ redisKey = utils.concatStrings({prefix, ":resource:", resource})
+ else
+ request.err(400, "\"resource\" missing from request body.")
+ end
+ elseif decoded.scope == "api" then
+ if apiId ~= nil then
+ redisKey = utils.concatStrings({prefix, ":api:", apiId})
+ else
+ request.err(400, "\"apiId\" missing from request body.")
+ end
+ else
+ request.err(400, "Invalid scope")
+ end
+ redisKey = utils.concatStrings({redisKey, ":key:", decoded.key})
+ return redisKey
+end
+
+return _M
\ No newline at end of file
diff --git a/api-gateway-config/scripts/lua/management/tenants.lua b/api-gateway-config/scripts/lua/management/tenants.lua
new file mode 100644
index 0000000..bab2986
--- /dev/null
+++ b/api-gateway-config/scripts/lua/management/tenants.lua
@@ -0,0 +1,269 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module tenants
+-- Management interface for tenants for the gateway
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local REDIS_HOST = os.getenv("REDIS_HOST")
+local REDIS_PORT = os.getenv("REDIS_PORT")
+local REDIS_PASS = os.getenv("REDIS_PASS")
+
+local _M = {};
+
+--- Add a tenant to the Gateway
+-- PUT /v1/tenants
+-- body:
+-- {
+-- "namespace": *(String) tenant namespace
+-- "instance": *(String) tenant instance
+-- }
+function _M.addTenant()
+ -- Open connection to redis or use one from connection pool
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ -- Check for tenant id and use existingTenant if it already exists in redis
+ local uri = string.gsub(ngx.var.request_uri, "?.*", "")
+ local existingTenant = checkURIForExistingTenant(red, uri)
+ -- Read in the PUT JSON Body
+ ngx.req.read_body()
+ local args = ngx.req.get_body_data()
+ if not args then
+ request.err(400, "Missing request body")
+ end
+ -- Convert json into Lua table
+ local decoded = cjson.decode(args)
+ -- Check for tenant id in JSON body
+ if existingTenant == nil and decoded.id ~= nil then
+ existingTenant = redis.getTenant(red, decoded.id)
+ if existingTenant == nil then
+ request.err(404, utils.concatStrings({"Unknown Tenant id ", decoded.id}))
+ end
+ end
+ -- Error checking
+ local fields = {"namespace", "instance"}
+ for k, v in pairs(fields) do
+ if not decoded[v] then
+ request.err(400, utils.concatStrings({"Missing field '", v, "' in request body."}))
+ end
+ end
+ -- Return tenant object
+ local uuid = existingTenant ~= nil and existingTenant.id or utils.uuid()
+ local tenantObj = {
+ id = uuid,
+ namespace = decoded.namespace,
+ instance = decoded.instance
+ }
+ tenantObj = redis.addTenant(red, uuid, tenantObj)
+ redis.close(red)
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, tenantObj)
+end
+
+--- Check for tenant id from uri and use existing tenant if it already exists in redis
+-- @param red Redis client instance
+-- @param uri Uri of request. Eg. /v1/tenants/{id}
+function checkURIForExistingTenant(red, uri)
+ local id, existing
+ local index = 1
+ -- Check if id is in the uri
+ for word in string.gmatch(uri, '([^/]+)') do
+ if index == 3 then
+ id = word
+ end
+ index = index + 1
+ end
+ -- Get object from redis
+ if id ~= nil then
+ existing = redis.getTenant(red, id)
+ if existing == nil then
+ request.err(404, utils.concatStrings({"Unknown Tenant id ", id}))
+ end
+ end
+ return existing
+end
+
+--- Get one or all tenants from the gateway
+-- GET /v1/tenants
+function _M.getTenants()
+ local uri = string.gsub(ngx.var.request_uri, "?.*", "")
+ local queryParams = ngx.req.get_uri_args()
+ local id
+ local index = 1
+ local apiQuery = false
+ for word in string.gmatch(uri, '([^/]+)') do
+ if index == 3 then
+ id = word
+ elseif index == 4 then
+ if word:lower() == 'apis' then
+ apiQuery = true
+ else
+ request.err(400, "Invalid request")
+ end
+ end
+ index = index + 1
+ end
+ if id == nil then
+ getAllTenants(queryParams)
+ else
+ if apiQuery == false then
+ getTenant(id)
+ else
+ getTenantAPIs(id, queryParams)
+ end
+ end
+end
+
+--- Get all tenants in redis
+-- @param queryParams object containing optional query parameters
+function getAllTenants(queryParams)
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local tenants = redis.getAllTenants(red)
+ redis.close(red)
+ local tenantList
+ if next(queryParams) ~= nil then
+ tenantList = filterTenants(tenants, queryParams);
+ end
+ if tenantList == nil then
+ tenantList = {}
+ for k, v in pairs(tenants) do
+ if k%2 == 0 then
+ tenantList[#tenantList+1] = cjson.decode(v)
+ end
+ end
+ tenantList = cjson.encode(tenantList)
+ end
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, tenantList)
+end
+
+--- Filter tenants based on query parameters
+-- @param tenants list of tenants
+-- @param queryParams query parameters to filter tenants
+function filterTenants(tenants, queryParams)
+ local namespace = queryParams.namespace
+ local instance = queryParams.instance
+ -- missing or invalid query parameters
+ if (namespace == nil and instance == nil) or (instance ~= nil and namespace == nil) then
+ return nil
+ end
+ -- filter tenants
+ local tenantList = {}
+ for k, v in pairs(tenants) do
+ if k%2 == 0 then
+ local tenant = cjson.decode(v)
+ if (namespace ~= nil and instance == nil and tenant.namespace == namespace) or
+ (namespace ~= nil and instance ~= nil and tenant.namespace == namespace and tenant.instance == instance) then
+ tenantList[#tenantList+1] = tenant
+ end
+ end
+ end
+ return cjson.encode(tenantList)
+end
+
+--- Get tenant by its id
+-- @param id tenant id
+function getTenant(id)
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local tenant = redis.getTenant(red, id)
+ if tenant == nil then
+ request.err(404, utils.concatStrings({"Unknown tenant id ", id }))
+ end
+ redis.close(red)
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, cjson.encode(tenant))
+end
+
+--- Get APIs associated with tenant
+-- @param id tenant id
+-- @param queryParams object containing optional query parameters
+function getTenantAPIs(id, queryParams)
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local apis = redis.getAllAPIs(red)
+ redis.close(red)
+ local apiList
+ if next(queryParams) ~= nil then
+ apiList = filterTenantAPIs(id, apis, queryParams);
+ end
+ if apiList == nil then
+ apiList = {}
+ for k, v in pairs(apis) do
+ if k%2 == 0 then
+ local decoded = cjson.decode(v)
+ if decoded.tenantId == id then
+ apiList[#apiList+1] = decoded
+ end
+ end
+ end
+ apiList = cjson.encode(apiList)
+ end
+ ngx.header.content_type = "application/json; charset=utf-8"
+ request.success(200, apiList)
+end
+
+--- Filter apis based on query paramters
+-- @param queryParams query parameters to filter apis
+function filterTenantAPIs(id, apis, queryParams)
+ local basePath = queryParams.basePath
+ local name = queryParams.name
+ -- missing query parameters
+ if (basePath == nil and name == nil)then
+ return nil
+ end
+ -- filter apis
+ local apiList = {}
+ for k, v in pairs(apis) do
+ if k%2 == 0 then
+ local api = cjson.decode(v)
+ if api.tenantId == id and
+ ((basePath ~= nil and name == nil and api.basePath == basePath) or
+ (name ~= nil and basePath == nil and api.name == name) or
+ (basePath ~= nil and name ~= nil and api.basePath == basePath and api.name == name)) then
+ apiList[#apiList+1] = api
+ end
+ end
+ end
+ return cjson.encode(apiList)
+end
+
+--- Delete tenant from gateway
+-- DELETE /v1/tenants/<id>
+function _M.deleteTenant()
+ local uri = string.gsub(ngx.var.request_uri, "?.*", "")
+ local index = 1
+ local id
+ for word in string.gmatch(uri, '([^/]+)') do
+ if index == 3 then
+ id = word
+ end
+ index = index + 1
+ end
+ if id == nil then
+ request.err(400, "No id specified.")
+ end
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 1000)
+ redis.deleteTenant(red, id)
+ redis.close(red)
+ request.success(200, {})
+end
+
+return _M
\ No newline at end of file
diff --git a/doc/routes.md b/doc/routes.md
index dc9a2a5..a2bb65a 100644
--- a/doc/routes.md
+++ b/doc/routes.md
@@ -237,7 +237,7 @@
## Subscriptions
-### PUT /subscriptions
+### PUT /v1/subscriptions
Add/update an api key for the specified tenant, resource, or api.
_body:_
@@ -256,7 +256,7 @@
Subscription created.
```
-### DELETE /subscriptions
+### DELETE /v1/subscriptions
Delete an api key associated with the specified tenant, resource or api.
_body:_