Skip to content

Commit

Permalink
Merge pull request #50 from openwhisk/develop
Browse files Browse the repository at this point in the history
Redis, filemgmt, management interface refactor
  • Loading branch information
mhamann authored Dec 18, 2016
2 parents 31771c1 + 50443fd commit 9a28451
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 240 deletions.
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ docker run -p 80:80 -p <managedurl_port>:8080 -p 9000:9000 \

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).
Expand All @@ -50,12 +50,7 @@ See [here](doc/routes.md) for the management interface for creating tenants/APIs
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

Expand Down
5 changes: 5 additions & 0 deletions api-gateway-config/api-gateway.conf
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ pcre_jit on;

http {
default_type text/plain;

# Set in-memory buffer size
client_body_buffer_size 1M;
client_max_body_size 1M;

# include all APIs being proxied
include /etc/api-gateway/conf.d/*.conf;
include /etc/api-gateway/generated-conf.d/*.conf;
Expand Down
16 changes: 14 additions & 2 deletions api-gateway-config/conf.d/managed_endpoints.conf
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ server {
location /50x.html {
more_set_headers 'Content-Type: application/json';
more_set_headers 'X-Request-Id: $requestId';
more_set_headers 'X-Gateway-Host: $gateway_host';
return 500 '{"code":$status, "message":"Oops. Something went wrong. Check your URI and try again."}\n';
}

Expand All @@ -57,11 +58,22 @@ server {
';
}

location ~ "^/api/([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\/\-\_\{\} ]+)(\\b)" {
set $upstream https://172.17.0.1;
set $tenant $1;
set $backendUrl '';
set $gatewayPath $2;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS';
access_by_lua_block {
local routing = require "routing"
routing.processCall()
}
proxy_pass $upstream;
}

location = /health {
proxy_pass http://0.0.0.0:9000/v1/health-check;
}

include /etc/api-gateway/managed_confs/*/*.conf;
}


Expand Down
105 changes: 8 additions & 97 deletions api-gateway-config/scripts/lua/lib/redis.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@
-- @author Alex Song (songs)

local cjson = require "cjson"
local filemgmt = require "lib/filemgmt"
local utils = require "lib/utils"
local logger = require "lib/logger"
local request = require "lib/request"

local REDIS_FIELD = "resources"
local BASE_CONF_DIR = "/etc/api-gateway/managed_confs/"

local _M = {}

Expand All @@ -44,7 +42,7 @@ local _M = {}
-- @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
Expand Down Expand Up @@ -215,18 +213,19 @@ function _M.getResource(red, key, field)
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
Expand Down Expand Up @@ -353,97 +352,9 @@ function _M.deleteSubscription(red, key)
end
end

-----------------------------------
------- Pub/Sub with Redis --------
-----------------------------------

local syncStatus = false
--- Sync with redis on startup and create conf files for resources that are already in redis
-- @param red redis client instance
function _M.syncWithRedis(red)
logger.info("Sync with redis in progress...")
setSyncStatus(true)
local resourceKeys = getAllResourceKeys(red)
for k, resourceKey in pairs(resourceKeys) do
local prefix, tenant, gatewayPath = resourceKey:match("([^,]+):([^,]+):([^,]+)")
local resourceObj = _M.getResource(red, resourceKey, REDIS_FIELD)
filemgmt.createResourceConf(BASE_CONF_DIR, tenant, ngx.escape_uri(gatewayPath), resourceObj)
end
os.execute("/usr/local/sbin/nginx -s reload")
setSyncStatus(false)
logger.info("All resources synced.")
end

function setSyncStatus(status)
syncStatus = status
end

function getSyncStatus()
return syncStatus
end

--- 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
function _M.subscribe(redisSubClient, redisGetClient)
logger.info("Subscribed to redis and listening for key changes...")
-- Subscribe to redis using psubscribe
local ok, err = redisSubClient:config("set", "notify-keyspace-events", "KEA")
if not ok then
request.err(500, utils.concatStrings({"Failed to subscribe to redis: ", err}))
end
ok, err = redisSubClient:psubscribe("__keyspace@0__:resources:*:*")
if not ok then
request.err(500, utils.concatStrings({"Failed to subscribe to redis: ", err}))
end
-- Update nginx conf file when redis is updated
local redisUpdated = false
local startTime = ngx.now()
while true do
local res, err = redisSubClient:read_reply()
if not res then
if err ~= "timeout" then
request.err(500, utils.concatStrings({"Failed to read from redis: ", err}))
end
else
-- res[3] format is "__keyspace@0__:resources:<tenantId>:<gatewayPath>"
local keyspacePrefix, resourcePrefix, tenant, gatewayPath = res[3]:match("([^,]+):([^,]+):([^,]+):([^,]+)")
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 \"'\"."}))
else
local resourceObj = _M.getResource(redisGetClient, redisKey, REDIS_FIELD)
if resourceObj == nil then
logger.debug(utils.concatStrings({"Redis key deleted: ", redisKey}))
local fileLocation = filemgmt.deleteResourceConf(BASE_CONF_DIR, tenant, ngx.escape_uri(gatewayPath))
logger.debug(utils.concatStrings({"Deleted file: ", fileLocation}))
else
logger.debug(utils.concatStrings({"Redis key updated: ", redisKey}))
local fileLocation = filemgmt.createResourceConf(BASE_CONF_DIR, tenant, ngx.escape_uri(gatewayPath), resourceObj)
logger.debug(utils.concatStrings({"Updated file: ", fileLocation}))
end
redisUpdated = true
end
end
-- reload Nginx only if redis has been updated and it has been at least 1 second since last reload
local timeDiff = ngx.now() - startTime
if(redisUpdated == true and timeDiff >= 1) then
os.execute("/usr/local/sbin/nginx -s reload")
logger.info("Nginx reloaded.")
redisUpdated = false
startTime = ngx.now()
end
end
end

--- Get gateway sync status
--- Check health of gateway
function _M.healthCheck()
if getSyncStatus() == true then
request.success(503, "Status: Gateway syncing.")
else
request.success(200, "Status: Gateway ready.")
end
request.success(200, "Status: Gateway ready.")
end

return _M
13 changes: 13 additions & 0 deletions api-gateway-config/scripts/lua/lib/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,23 @@ function uuid()
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

Loading

0 comments on commit 9a28451

Please sign in to comment.