Implement new path parameter mapping
diff --git a/README.md b/README.md
index 4fe1392..2a59f2f 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@
This command starts an API Gateway that subscribes to the Redis instance with the specified host and port. The `REDIS_PASS` variable is optional and is required only when redis needs authentication.
-On startup, the API Gateway looks for pre-existing resources in redis, whose keys are defined as `resources:<namespace>:<resource>`, and creates nginx conf files associated with those resources. Then, it listens for any resource key changes in redis and updates nginx conf files appropriately. These conf files are stored in the running docker container at `/etc/api-gateway/managed_confs/<namespace>/<resource>.conf`.
+On startup, the API Gateway subscribes to redis and listens for changes in keys that are defined as `resources:<namespace>:<resourcePath>`.
## Routes
See [here](doc/routes.md) for the management interface for creating tenants/APIs. For detailed API policy definitions, see [here](doc/policies.md).
@@ -50,12 +50,7 @@
make docker-run PUBLIC_MANAGEDURL_HOST=<mangedurl_host> PUBLIC_MANAGEDURL_PORT=<managedurl_port> \
REDIS_HOST=<redis_host> REDIS_PORT=<redis_port> REDIS_PASS=<redis_pass>
```
-
- The main API Gateway process is exposed to port `80`. To test that the Gateway works see its `health-check`:
- ```
- $ curl http://<docker_host_ip>/health-check
- API-Platform is running!
- ```
+
### Testing
diff --git a/api-gateway-config/conf.d/managed_endpoints.conf b/api-gateway-config/conf.d/managed_endpoints.conf
index 8d459db..37a5ed5 100644
--- a/api-gateway-config/conf.d/managed_endpoints.conf
+++ b/api-gateway-config/conf.d/managed_endpoints.conf
@@ -58,7 +58,7 @@
';
}
- location ~ ^/api/([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\/\-\_\{\}]+)(\\b) {
+ location ~ "^/api/([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\/\-\_\{\} ]+)(\\b)" {
set $upstream https://172.17.0.1;
set $tenant $1;
set $backendUrl '';
diff --git a/api-gateway-config/scripts/lua/lib/redis.lua b/api-gateway-config/scripts/lua/lib/redis.lua
index dfdcb7a..2007b14 100644
--- a/api-gateway-config/scripts/lua/lib/redis.lua
+++ b/api-gateway-config/scripts/lua/lib/redis.lua
@@ -42,7 +42,7 @@
-- @param timeout redis timeout in milliseconds
function _M.init(host, port, password, timeout)
local redis = require "resty.redis"
- local red = redis:new()
+ local red = redis:new()
red:set_timeout(timeout)
-- Connect to Redis server
local retryCount = 4
@@ -213,18 +213,19 @@
return resourceObj
end
---- Get all resource keys in redis
+--- Get all resource keys for a tenant in redis
-- @param red redis client instance
-function getAllResourceKeys(red)
+-- @param tenantId tenant id
+function _M.getAllResourceKeys(red, tenantId)
-- Find all resourceKeys in redis
- local resources, err = red:scan(0, "match", "resources:*:*")
+ local resources, err = red:scan(0, "match", utils.concatStrings({"resources:", tenantId, ":*"}))
if not resources then
request.err(500, util.concatStrings({"Failed to retrieve resource keys: ", err}))
end
local cursor = resources[1]
local resourceKeys = resources[2]
while cursor ~= "0" do
- resources, err = red:scan(cursor, "match", "resources:*:*")
+ resources, err = red:scan(cursor, "match", utils.concatStrings({"resources:", tenantId, ":*"}))
if not resources then
request.err(500, util.concatStrings({"Failed to retrieve resource keys: ", err}))
end
@@ -355,7 +356,6 @@
------- Pub/Sub with Redis --------
-----------------------------------
-local gatewayReady = false
--- Subscribe to redis
-- @param redisSubClient the redis client that is listening for the redis key changes
-- @param redisGetClient the redis client that gets the changed resource to update the conf file
@@ -371,7 +371,6 @@
request.err(500, utils.concatStrings({"Failed to subscribe to redis: ", err}))
end
while true do
- gatewayReady = true
local res, err = redisSubClient:read_reply()
if not res then
if err ~= "timeout" then
@@ -383,13 +382,13 @@
local redisKey = utils.concatStrings({resourcePrefix, ":", tenant, ":", gatewayPath})
-- Don't allow single quotes in the gateway path
if string.match(gatewayPath, "'") then
- logger.debug(utils.concatStrings({"Redis key \"", redisKey, "\" contains illegal character \"'\"."}))
+ logger.info(utils.concatStrings({"Redis key \"", redisKey, "\" contains illegal character \"'\"."}))
else
local resourceObj = _M.getResource(redisGetClient, redisKey, REDIS_FIELD)
if resourceObj == nil then
- logger.debug(utils.concatStrings({"Redis key deleted: ", redisKey}))
+ logger.info(utils.concatStrings({"Redis key deleted: ", redisKey}))
else
- logger.debug(utils.concatStrings({"Redis key updated: ", redisKey}))
+ logger.info(utils.concatStrings({"Redis key updated: ", redisKey}))
end
end
end
@@ -398,11 +397,7 @@
--- Get gateway sync status
function _M.healthCheck()
- if gatewayReady == false then
- request.success(503, "Status: Gateway starting up.")
- else
- request.success(200, "Status: Gateway ready.")
- end
+ request.success(200, "Status: Gateway ready.")
end
return _M
\ No newline at end of file
diff --git a/api-gateway-config/scripts/lua/lib/utils.lua b/api-gateway-config/scripts/lua/lib/utils.lua
index cdaec4e..d757c39 100644
--- a/api-gateway-config/scripts/lua/lib/utils.lua
+++ b/api-gateway-config/scripts/lua/lib/utils.lua
@@ -84,10 +84,23 @@
end)
end
+--- Check if element exists in table as value
+-- @param table table to check
+-- @param element element to check in table
+function tableContains(table, element)
+ for i, value in pairs(table) do
+ if value == element then
+ return true
+ end
+ end
+ 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
index a720883..8108598 100644
--- a/api-gateway-config/scripts/lua/management.lua
+++ b/api-gateway-config/scripts/lua/management.lua
@@ -44,7 +44,7 @@
--------------------------
--- Add an api to the Gateway
--- PUT /APIs
+-- PUT /v1/apis
-- body:
-- {
-- "name": *(String) name of API
@@ -83,6 +83,7 @@
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})
@@ -101,7 +102,8 @@
managedUrlObj = redis.addAPI(red, uuid, managedUrlObj, existingAPI)
-- Add resources to redis
for path, resource in pairs(decoded.resources) do
- local gatewayPath = utils.concatStrings({basePath, ngx.escape_uri(path)})
+ 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)
@@ -229,7 +231,7 @@
-- @param tenantId
function addResource(red, resource, gatewayPath, tenantId)
-- Create resource object and add to redis
- local redisKey = utils.concatStrings({"resources", ":", tenantId, ":", ngx.unescape_uri(gatewayPath)})
+ local redisKey = utils.concatStrings({"resources", ":", tenantId, ":", gatewayPath})
local apiId
local operations
for k, v in pairs(resource) do
@@ -244,7 +246,7 @@
end
--- Get one or all APIs from the gateway
--- GET /APIs
+-- GET /v1/apis
function _M.getAPIs()
local uri = string.gsub(ngx.var.request_uri, "?.*", "")
local id
@@ -321,7 +323,7 @@
end
--- Delete API from gateway
--- DELETE /APIs/<id>
+-- DELETE /v1/apis/<id>
function _M.deleteAPI()
local uri = string.gsub(ngx.var.request_uri, "?.*", "")
local index = 1
@@ -345,7 +347,8 @@
-- 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, ngx.escape_uri(path)})
+ 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)
@@ -357,7 +360,7 @@
-- @param gatewayPath path in gateway
-- @param tenantId tenant id
function deleteResource(red, gatewayPath, tenantId)
- local redisKey = utils.concatStrings({"resources:", tenantId, ":", ngx.unescape_uri(gatewayPath)})
+ local redisKey = utils.concatStrings({"resources:", tenantId, ":", gatewayPath})
redis.deleteResource(red, redisKey, REDIS_FIELD)
end
@@ -366,7 +369,7 @@
-----------------------------
--- Add a tenant to the Gateway
--- PUT /Tenants
+-- PUT /v1/tenants
-- body:
-- {
-- "namespace": *(String) tenant namespace
@@ -414,7 +417,7 @@
end
--- Get one or all tenants from the gateway
--- GET /Tenants
+-- GET /v1/tenants
function _M.getTenants()
local uri = string.gsub(ngx.var.request_uri, "?.*", "")
local id
@@ -493,7 +496,7 @@
end
--- Delete tenant from gateway
--- DELETE /Tenants/<id>
+-- DELETE /v1/tenants/<id>
function _M.deleteTenant()
local uri = string.gsub(ngx.var.request_uri, "?.*", "")
local index = 1
@@ -521,7 +524,6 @@
-- GET /v1/sync
function _M.sync()
local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- logger.info(utils.concatStrings({"Connected to redis at ", REDIS_HOST, ":", REDIS_PORT}))
redis.syncWithRedis(red)
ngx.exit(200)
end
@@ -531,6 +533,7 @@
function _M.subscribe()
local redisGetClient = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
local redisSubClient = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 1000)
+ logger.info(utils.concatStrings({"Connected to redis at ", REDIS_HOST, ":", REDIS_PORT}))
redis.subscribe(redisSubClient, redisGetClient)
ngx.exit(200)
end
diff --git a/api-gateway-config/scripts/lua/policies/mapping.lua b/api-gateway-config/scripts/lua/policies/mapping.lua
index 6748fe3..97f7c71 100644
--- a/api-gateway-config/scripts/lua/policies/mapping.lua
+++ b/api-gateway-config/scripts/lua/policies/mapping.lua
@@ -70,7 +70,7 @@
--- Insert parameter value to header, body, or query params into request
-- @param m Parameter value to add to request
function insertParam(m)
- local v = nil
+ local v
local k = m.to.name
if m.from.value ~= nil then
v = m.from.value
@@ -81,7 +81,7 @@
elseif m.from.location == 'body' then
v = body[m.from.name]
elseif m.from.location == 'path' then
- v = ngx.var[utils.concatStrings({'path_', m.from.name})]
+ v = ngx.ctx[m.from.name]
end
-- determine to where
if m.to.location == 'header' then
@@ -208,8 +208,8 @@
function insertPath(k, v)
v = ngx.unescape_uri(v)
- local primedUri = path:gsub("%{(%w*)%}", v)
- ngx.req.set_uri(primedUri)
+ path = path:gsub(utils.concatStrings({"%{", k ,"%}"}), v)
+ ngx.req.set_uri(path)
end
function removeHeader(k)
diff --git a/api-gateway-config/scripts/lua/routing.lua b/api-gateway-config/scripts/lua/routing.lua
index ab62bb7..1b896dd 100644
--- a/api-gateway-config/scripts/lua/routing.lua
+++ b/api-gateway-config/scripts/lua/routing.lua
@@ -31,6 +31,7 @@
local security = require "policies/security"
local mapping = require "policies/mapping"
local rateLimit = require "policies/rateLimit"
+local logger = require "lib/logger"
local REDIS_HOST = os.getenv("REDIS_HOST")
local REDIS_PORT = os.getenv("REDIS_PORT")
@@ -40,12 +41,17 @@
--- Main function that handles parsing of invocation details and carries out implementation
function processCall()
- -- Handle path parameters
- ngx.var.gatewayPath = ngx.unescape_uri(ngx.var.gatewayPath):gsub("%{(%w*)%}", utils.convertTemplatedPathParam)
-- Get resource object from redis
local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
local redisKey = utils.concatStrings({"resources:", ngx.var.tenant, ":", ngx.var.gatewayPath})
local obj = redis.getResource(red, redisKey, "resources")
+ -- Check for path parameters
+ if obj == nil then
+ obj = checkForPathParams(red)
+ if obj == nil then
+ return request.err(404, 'API doesn\'t exist.')
+ end
+ end
obj = cjson.decode(obj)
local found = false
for verb, opFields in pairs(obj.operations) do
@@ -82,6 +88,33 @@
end
end
+--- Check redis for path parameters
+-- @param red redis client instance
+function checkForPathParams(red)
+ local resourceKeys = redis.getAllResourceKeys(red, ngx.var.tenant)
+ for i, key in pairs(resourceKeys) do
+ local res = {string.match(key, "([^,]+):([^,]+):([^,]+)")}
+ local path = res[3] -- gatewayPath portion of redis key
+ local pathParamVars = {}
+ for w in string.gfind(path, "({%w+})") do
+ w = string.gsub(w, "{", "")
+ w = string.gsub(w, "}", "")
+ pathParamVars[#pathParamVars + 1] = w
+ end
+ if next(pathParamVars) ~= nil then
+ local pathPattern, count = string.gsub(path, "%{(%w*)%}", "([^,]+)")
+ local obj = {string.match(ngx.var.gatewayPath, pathPattern)}
+ if (#obj == count) then
+ for i, v in pairs(obj) do
+ ngx.ctx[pathParamVars[i]] = v
+ end
+ return redis.getResource(red, key, "resources")
+ end
+ end
+ end
+ return nil
+end
+
--- Function to read the list of policies and send implementation to the correct backend
-- @param obj List of policies containing a type and value field. This function reads the type field and routes it appropriately.
-- @param apiKey optional subscription api key
@@ -98,34 +131,24 @@
--- Given a verb, transforms the backend request to use that method
-- @param v Verb to set on the backend request
function setVerb(v)
- if (string.lower(v) == 'post') then
- ngx.req.set_method(ngx.HTTP_POST)
- elseif (string.lower(v) == 'put') then
- ngx.req.set_method(ngx.HTTP_PUT)
- elseif (string.lower(v) == 'delete') then
- ngx.req.set_method(ngx.HTTP_DELETE)
- elseif (string.lower(v) == 'patch') then
- ngx.req.set_method(ngx.HTTP_PATCH)
- elseif (string.lower(v) == 'head') then
- ngx.req.set_method(ngx.HTTP_HEAD)
- elseif (string.lower(v) == 'options') then
- ngx.req.set_method(ngx.HTTP_OPTIONS)
+ local allowedVerbs = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'}
+ local verb = string.upper(v)
+ if(utils.tableContains(allowedVerbs, verb)) then
+ ngx.req.set_method(ngx[utils.concatStrings({"HTTP_", verb})])
else
ngx.req.set_method(ngx.HTTP_GET)
end
end
function getUriPath(backendPath)
- local uriPath
- local i, j = ngx.var.uri:find(ngx.var.gatewayPath)
+ local i, j = ngx.var.uri:find(ngx.unescape_uri(ngx.var.gatewayPath))
local incomingPath = ((j and ngx.var.uri:sub(j + 1)) or nil)
-- Check for backendUrl path
- if backendPath == nil or backendPath== '' or backendPath== '/' then
- uriPath = (incomingPath and incomingPath ~= '') and incomingPath or '/'
+ if backendPath == nil or backendPath == '' or backendPath == '/' then
+ return (incomingPath and incomingPath ~= '') and incomingPath or '/'
else
- uriPath = utils.concatStrings({backendPath, incomingPath})
+ return utils.concatStrings({backendPath, incomingPath})
end
- return uriPath
end
_M.processCall = processCall