Browse Source

first commit

master
Nicolas Massé 8 years ago
commit
718f529192
  1. 21
      LICENSE
  2. 47
      README.md
  3. 54
      apicast-module/README.md
  4. 11
      apicast-module/dynamic-router-upstream.conf
  5. 24
      apicast-module/dynamic-router.conf
  6. 89
      apicast-module/dynamic-router.lua
  7. 65
      catalog/README.md
  8. 35
      catalog/catalog.conf
  9. 22
      catalog/catalog.lua
  10. 33
      catalog/config.json

21
LICENSE

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Nicolas MASSE
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.

47
README.md

@ -0,0 +1,47 @@
# Dynamic Routing for Apicast
## Introduction
This project provides a dynamic routing module for Apicast. It routes the
API request to the appropriate backend, based on an HTTP Header of the request.
A sample use case could be:
- A Load Balancer in front of Apicast identifies the source of the request (`internal`/`external` or `dev`/`prod`)
- The LB add the corresponding header (`x-env: dev` or `x-env: prod` for instance)
- Based on this header, the Apicast routes the API request to the corresponding backend
The API Backends are discovered by querying a Service Catalog. A sample service
catalog is given with this project.
It is designed to be hosted on Apicast itself (or any nginx instance) in order
to simplify the deployment.
## Deployment
Put `dynamic-router.conf` in `/opt/app-root/src/apicast.d/dynamic-router.conf`:
```
oc create configmap apicast.d --from-file=apicast-module/dynamic-router.conf
oc volume dc/apicast-staging --add --name=apicastd --mount-path /opt/app-root/src/apicast.d/ --type=configmap --configmap-name=apicast.d
```
Put `dynamic-router-upstream.conf` and `catalog.conf` in `/opt/app-root/src/sites.d/`:
```
oc create configmap sites.d --from-file=apicast-module/dynamic-router-upstream.conf --from-file=catalog/catalog.conf
oc volume dc/apicast-staging --add --name=sitesd --mount-path /opt/app-root/src/sites.d/ --type=configmap --configmap-name=sites.d
```
Put `catalog.lua` and `dynamic-router.lua` in `/opt/app-root/src/src/custom/`:
```
oc create configmap apicast-custom-module --from-file=apicast-module/dynamic-router.lua --from-file=catalog/catalog.lua
oc volume dc/apicast-staging --add --name=apicast-custom-module --mount-path /opt/app-root/src/src/custom/ --type=configmap --configmap-name=apicast-custom-module
```
Set the configuration required by the catalog and the dynamic routing module as environment variables and re-deploy apicast:
```
oc env dc/apicast-staging APICAST_CUSTOM_CONFIG=custom/dynamic-router
oc env dc/apicast-staging DYNAMIC_ROUTER_CATALOG_URL=http://127.0.0.1:8082
oc env dc/apicast-staging DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME=x-env
oc rollout latest apicast-staging
```
Once, you get it to work on `apicast-staging`, you can do the same on `apicast-production`.

54
apicast-module/README.md

@ -0,0 +1,54 @@
# A dynamic routing module for Apicast
## Introduction
This project provides a dynamic routing module for Apicast. It routes the
API request to the appropriate backend, based on an HTTP Header of the request.
A sample use case could be:
- A Load Balancer identifies the source of the request (internal / external for instance)
- The LB add the corresponding header (`x-env: dev` or `x-env: prod`)
- Based on this header, the Apicast routes the API request to the corresponding backend
The API Backends are discovered by querying a Service Catalog. A sample service
catalog is given with this project.
## Development
First of all, setup your development environment as explained [here](https://github.com/3scale/apicast/tree/master#development--testing).
Then, issue the following commands:
```
git clone https://github.com/nmasse-itix/apicast-dynamic-router.git
git clone https://github.com/3scale/apicast.git
cd apicast
luarocks make apicast/*.rockspec --local
ln -s $PWD/../apicast-dynamic-router/apicast-module/dynamic-router.conf apicast/apicast.d/dynamic-router.conf
ln -s $PWD/../apicast-dynamic-router/apicast-module/dynamic-router-upstream.conf apicast/sites.d/dynamic-router-upstream.conf
mkdir -p custom
ln -s $PWD/../apicast-dynamic-router/apicast-module/dynamic-router.lua custom/dynamic-router.lua
```
Configure your apicast as explained [here](https://github.com/3scale/apicast/blob/master/doc/parameters.md)
and [here](https://github.com/3scale/apicast/blob/master/doc/configuration.md).
```
export APICAST_CUSTOM_CONFIG=custom/dynamic-router
export DYNAMIC_ROUTER_CATALOG_URL=http://127.0.0.1:8082
export DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME=x-env
```
Finally, launch apicast:
```
bin/apicast -i 0 -m off
```
## Testing
The default catalog (`catalog.lua`) and the default configuration (`config.json`)
provide a few examples that you can test:
```
curl -D - http://localhost:8080/echo
curl -D - http://localhost:8080/echo -H "x-env: prod"
curl -D - http://localhost:8080/echo -H "x-env: dev"
curl -D - http://localhost:8080/echo -H "x-env: bogus"
```

11
apicast-module/dynamic-router-upstream.conf

@ -0,0 +1,11 @@
upstream catalog_upstream {
server 0.0.0.1:1;
balancer_by_lua_block {
local balancer = require "balancer"
balancer.call()
}
keepalive 1024;
}

24
apicast-module/dynamic-router.conf

@ -0,0 +1,24 @@
location = /dummy {
set $catalog_url "http://catalog_upstream";
set $catalog_host "catalog_upstream";
set $service_name "dummy";
set $environment "bogus";
}
location = /dynamic-router {
internal;
set $path /catalog/services/$service_name/environments/$environment/target;
proxy_pass $catalog_url$path;
proxy_pass_request_headers off;
proxy_pass_request_body off;
proxy_http_version 1.1;
proxy_set_header Host "$catalog_host";
proxy_set_header Connection "";
proxy_set_header Content-Length "";
#rewrite_by_lua_block {
# ngx.log(ngx.WARN, "service_name = " .. ngx.var.service_name)
# ngx.var.real_url = ngx.var.catalog_url .. ngx.var.path
#}
}

89
apicast-module/dynamic-router.lua

@ -0,0 +1,89 @@
local resty_resolver = require 'resty.resolver'
local resty_url = require 'resty.url'
local _M = { }
local service_catalog_url
local environment_header_name
function _M.setup(proxy)
-- In case of error during initialization, we will fallback to the default behavior
local error = false
-- Get configuration from Environment Variables
service_catalog_url = os.getenv('DYNAMIC_ROUTER_CATALOG_URL')
if (service_catalog_url == nil) then
ngx.log(ngx.ERR, "No environment variable DYNAMIC_ROUTER_CATALOG_URL.")
error = true
else
ngx.log(ngx.INFO, "Using the catalog at " .. (service_catalog_url or "nil"))
end
environment_header_name = os.getenv('DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME')
if (environment_header_name == nil) then
ngx.log(ngx.ERR, "No environment variable DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME.")
error = true
else
ngx.log(ngx.INFO, "Using the header " .. (environment_header_name or "nil") .. " as environment")
end
if not error then
-- Update the Proxy Metatable with our custom function
proxy.get_upstream = get_upstream
else
ngx.log(ngx.ERR, "Errors during initialization. Dynamic Routing disabled.")
end
end
function get_upstream(service)
service = service or ngx.ctx.service
ngx.log(ngx.DEBUG, "Dynamically routing service " .. (service.id or "none"))
local environment = ngx.req.get_headers()[string.lower(environment_header_name)]
if (environment == nil) then
ngx.log(ngx.WARN, "No header " .. environment_header_name .. " found, defaulting to '_default'.")
environment = "_default"
end
-- Split the Catalog URL into components
local url = resty_url.split(service_catalog_url)
local scheme, _, _, server, port, path =
url[1], url[2], url[3], url[4], url[5] or resty_url.default_port(url[1]), url[6] or ''
-- Resolve the DNS name of the Catalog Server
ngx.ctx.catalog_upstream = resty_resolver:instance():get_servers(server, { port = port })
-- Share those variables with the Nginx sub-request
local subrequest_vars = {
catalog_url = service_catalog_url,
catalog_host = server,
service_name = service.id,
environment = environment
}
ngx.log(ngx.INFO, 'Querying the Service Catalog at ', service_catalog_url, " for service id ", service.id, " and environment ", environment)
local res = ngx.location.capture("/dynamic-router", { vars = subrequest_vars, ctx = { catalog_upstream = ngx.ctx.catalog_upstream } })
local new_backend
if (res.status == 200) then
new_backend = res.body
ngx.log(ngx.INFO, "Found a backend for service " .. (service.id or "none") .. ": " .. new_backend)
else
-- In case we cannot get a positive answer from the Service Catalog, use the default API Backend.
new_backend = service.api_backend
ngx.log(ngx.ERR, "Could not get a positive response from the service catalog for service " .. (service.id or "none") .. ": HTTP " .. res.status)
end
-- Split the new Backend URL into components
local url = resty_url.split(new_backend)
local scheme, _, _, server, port, path =
url[1], url[2], url[3], url[4], url[5] or resty_url.default_port(url[1]), url[6] or ''
return {
server = server,
host = service.hostname_rewrite or server,
uri = scheme .. '://upstream' .. path,
port = tonumber(port)
}
end
return _M

65
catalog/README.md

@ -0,0 +1,65 @@
# A "Self-Contained" Service Catalog for Apicast
## Introduction
This project provides a Service Catalog (list of services with their associated
environments and the matching URLs.
It is designed to be hosted on Apicast itself (or any nginx instance) in order
to simplify the deployment.
## How it works
API Services, Environments and their backends are declared in the `catalog.lua`.
The service catalog listens on port 8082.
You can query the Service Catalog by issuing an HTTP Request:
```
GET /catalog/services/{service_id}/environments/{environment}/target
```
For instance, to query the `prod` environment of service `123`:
```
curl -D - http://localhost:8082/catalog/services/123/environments/prod/target
```
## Development
First of all, setup your development environment as explained [here](https://github.com/3scale/apicast/tree/master#development--testing).
Then, issue the following commands:
```
git clone https://github.com/nmasse-itix/apicast-dynamic-router.git
git clone https://github.com/3scale/apicast.git
cd apicast
luarocks make apicast/*.rockspec --local
ln -s $PWD/../apicast-dynamic-router/catalog/catalog.conf apicast/sites.d/catalog.conf
ln -s $PWD/../apicast-dynamic-router/catalog/config.json config.json
mkdir -p custom
ln -s $PWD/../apicast-dynamic-router/catalog/catalog.lua custom/catalog.lua
```
Configure your apicast as explained [here](https://github.com/3scale/apicast/blob/master/doc/parameters.md)
and [here](https://github.com/3scale/apicast/blob/master/doc/configuration.md).
```
export THREESCALE_DEPLOYMENT_ENV=sandbox
export THREESCALE_CONFIG_FILE=config.json
export APICAST_LOG_LEVEL=debug
```
Finally, launch apicast:
```
bin/apicast -i 0 -m off
```
## Testing
The default catalog (`catalog.lua`) provides a few examples that you can test:
```
curl -D - http://localhost:8082/catalog/services/123/environments/prod/target
curl -D - http://localhost:8082/catalog/services/123/environments/dev/target
curl -D - http://localhost:8082/catalog/services/123/environments/bogus/target
curl -D - http://localhost:8082/catalog/services/456/environments/prod/target
curl -D - http://localhost:8082/catalog/services/456/environments/dev/target
curl -D - http://localhost:8082/catalog/services/456/environments/bogus_but_should_work/target
```

35
catalog/catalog.conf

@ -0,0 +1,35 @@
server {
listen 8082;
location ~ ^/catalog/services/([^/]+)/environments/([^/]+)/target$ {
content_by_lua_block {
local service = ngx.var[1]
local variant = ngx.var[2]
ngx.log(ngx.DEBUG, "Service Catalog request for service = " .. (ngx.var[1] or "<NONE>") .. " and variant = " .. (ngx.var[2] or ""))
local catalog = require "custom/catalog"
if (catalog[service] ~= nil) then
local variants = catalog[service]
if (variants[variant] ~= nil) then
ngx.print(variants[variant])
ngx.exit(ngx.HTTP_OK)
else
-- is there a default environment ?
if (variants._default ~= nil) then
ngx.print(variants._default)
ngx.exit(ngx.HTTP_OK)
else
ngx.status = 404 -- Status needs to be set before the body
ngx.print("") -- Prevent default NGINX Error Page
ngx.exit(ngx.HTTP_NOT_FOUND)
end
end
else
ngx.status = 404 -- Status needs to be set before the body
ngx.print("") -- Prevent default NGINX Error Page
ngx.exit(ngx.HTTP_NOT_FOUND)
end
}
}
}

22
catalog/catalog.lua

@ -0,0 +1,22 @@
return {
-- First service
["123"] = {
-- Prod environment
prod = "https://echo-api.3scale.net",
-- Dev environment
dev = "http://echo-api.3scale.net"
},
-- Second service
["456"] = {
-- Prod environment
prod = "http://prod.myservice.corp",
-- Dev environment
dev = "http://dev.myservice.corp",
-- Default variant (if nothing matches): sandbox environment
_default = "http://sandbox.myservice.corp"
}
}

33
catalog/config.json

@ -0,0 +1,33 @@
{
"id": 1234567890987,
"provider_key": "provider-key",
"services": [
{
"id": 123,
"backend_version": "1",
"proxy": {
"api_backend": "http://127.0.0.1:8081",
"hosts": [
"localhost",
"127.0.0.1"
],
"backend": {
"endpoint": "http://127.0.0.1:8081",
"host": "backend"
},
"auth_user_key": "user_key",
"credentials_location": "query",
"proxy_rules": [
{
"http_method": "GET",
"pattern": "/",
"metric_system_name": "hits",
"delta": 1,
"parameters": [],
"querystring_parameters": {}
}
]
}
}
]
}
Loading…
Cancel
Save