diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000000..d74043e39e4
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,59 @@
+dist: xenial
+sudo: false
+
+language: generic
+
+jdk:
+ - oraclejdk8
+
+notifications:
+ email: false
+
+addons:
+ postgresql: "9.5"
+ apt:
+ packages:
+ - net-tools
+ - libpcre3-dev
+ - build-essential
+ hosts:
+ - grpcs_1.test
+ - grpcs_2.test
+
+services:
+ - docker
+
+env:
+ global:
+ - TEST_SUITE=integration
+ - INSTALL_CACHE=$HOME/install-cache
+ - DOWNLOAD_ROOT=$HOME/download-root
+ - PLUGIN_NAME=liamp
+ - KONG_PLUGINS=bundled,$PLUGIN_NAME
+ - KONG_TEST_PLUGINS=$KONG_PLUGINS
+ - TEST_FILE_PATH=$TRAVIS_BUILD_DIR/spec
+ - JOBS=2
+ matrix:
+ - KONG_TEST_DATABASE=cassandra CASSANDRA=2.2.12 KONG=master BUSTED_ARGS="-o gtest -v --exclude-tags=flaky,ipv6,postgres,off"
+ - KONG_TEST_DATABASE=cassandra CASSANDRA=3.9 KONG=master BUSTED_ARGS="-o gtest -v --exclude-tags=flaky,ipv6,postgres,off"
+ - KONG_TEST_DATABASE=postgres POSTGRES=9.5 KONG=master BUSTED_ARGS="-o gtest -v --exclude-tags=flaky,ipv6,cassandra,off"
+matrix:
+ allow_failures:
+ - env: KONG_TEST_DATABASE=postgres POSTGRES=9.5 KONG=master BUSTED_ARGS="-o gtest -v --exclude-tags=flaky,ipv6,cassandra,off"
+
+install:
+ - make setup-ci
+ - pushd kong-source && source .ci/setup_env.sh && popd
+ - pushd kong-source && make dev && popd
+ - cp -r kong-source/spec/fixtures spec
+ - luarocks make
+
+script:
+ - pushd kong-source && bin/busted $BUSTED_ARGS ../spec && popd
+
+cache:
+ apt: true
+ directories:
+ - $DOWNLOAD_CACHE
+ - $INSTALL_CACHE
+ - $HOME/.ccm/repository
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000000..00b20fb5f3f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+## 0.2.0
+
+### Changed
+
+- convert the plugin to the PDK and new DB (developed against Kong 1.x)
+
+## 0.1.0
+
+- Extended the `aws-lambda` plugin from the Kong/kong repository with added ECS IAM roles (developed against Kong 0.13)
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000000..05fce97e28e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
+KONG_SOURCE_LOCATION?=$(ROOT_DIR)/kong-source
+KONG?=master
+
+setup-kong:
+ -rm -rf $(KONG_SOURCE_LOCATION); \
+ git clone --branch $(KONG) https://github.com/Kong/kong.git $(KONG_SOURCE_LOCATION)
+
+setup-ci: setup-kong
+ cd $(KONG_SOURCE_LOCATION); \
+ $(MAKE) setup-ci
diff --git a/README.md b/README.md
index bb9d9447da1..6792c61d1d9 100644
--- a/README.md
+++ b/README.md
@@ -1,56 +1,138 @@
-# Introduction
+[![Build Status][badge-travis-image]][badge-travis-url]
-This is a custom version of the Lambda plugin.
+# kong-plugin-aws-lambda
-- allows for EC2 IAM roles for authorization, see https://github.com/Kong/kong/pull/2777
-- has a modified version of https://github.com/Kong/kong/pull/3639
-- added ECS IAM roles
+Invoke an [AWS Lambda](https://aws.amazon.com/lambda/) function from Kong. It can be used in combination with other request plugins to secure, manage or extend the function.
+## Configuration
-## Installation
+### Enabling the plugin on a Service
-Since it is a custom version, it should be installed under its own name. To
-facilitate this there is a rockspec file for use with LuaRocks.
+#### With a database
-Pack the rock (from `./kong/plugins/aws-lambda`):
+Configure this plugin on a [Service](https://docs.konghq.com/latest/admin-api/#service-object) by making the following request:
-```shell
-> luarocks make
-> luarocks pack kong-plugin-liamp
+```
+$ curl -X POST http://kong:8001/services/{service}/plugins \
+ --data name=aws-lambda \
+ --data "config.aws_region=AWS_REGION" \
+ --data "config.function_name=LAMBDA_FUNCTION_NAME"
```
-This results in a `rock` file: `kong-plugin-liamp-0.1.0-1.all.rock`
+#### Without a database
-This file can be installed on any Kong system with:
+Configure this plugin on a [Service](https://docs.konghq.com/latest/admin-api/#service-object) by adding this section do your declarative configuration file:
-```shell
-> luarocks install kong-plugin-liamp-0.1.0-1.all.rock
```
+plugins:
+- name: aws-lambda
+ service: {service}
+ config:
+ aws_region: AWS_REGION
+ function_name: LAMBDA_FUNCTION_NAME
+```
+
+In both cases, `{service}` is the `id` or `name` of the Service that this plugin configuration will target.
+
+
+### Enabling the plugin on a Route
-## Usage
+#### With a database
-Since it is renamed, it will not be enabled by default, hence it must be enabled
-like other custom plugins:
+Configure this plugin on a [Route](https://docs.konghq.com/latest/admin-api/#Route-object) with:
-```shell
-> export KONG_CUSTOM_PLUGINS=liamp
```
+$ curl -X POST http://kong:8001/routes/{route}/plugins \
+ --data name=aws-lambda \
+ --data "config.aws_region=AWS_REGION" \
+ --data "config.function_name=LAMBDA_FUNCTION_NAME"
+```
+
+#### Without a database
+
+Configure this plugin on a [Route](https://docs.konghq.com/latest/admin-api/#route-object) by adding this section do your declarative configuration file:
-Once enabled, it differs slightly from the original Lambda plugin in that the
-token and secret are no longer required when configuring the plugin.
-The behaviour is now to default to IAM roles, unless the secret and token
-are provided.
+```
+plugins:
+- name: aws-lambda
+ route: {route}
+ config:
+ aws_region: AWS_REGION
+ function_name: LAMBDA_FUNCTION_NAME
+```
-* When the IAM roles are used (default, if no token/secret is provided), the plugin
- will first try ECS metadata, and if not available it will fallback on EC2
- metadata.
+In both cases, `{route}` is the `id` or `name` of the Route that this plugin configuration will target.
-* the ability was added to also send very large bodies (that where buffered to
- disk) by Kong. To control this there is a new setting `skip_large_bodies` which
- defaults to `true`. Set it to `false` to enable it, but be aware that those
- very large bodies will have an impact on the system memory.
+### Enabling the plugin on a Consumer
-## Compatibility
+#### With a database
+
+You can use the `http://localhost:8001/plugins` endpoint to enable this plugin on specific [Consumers](https://docs.konghq.com/latest/admin-api/#Consumer-object):
+
+```
+$ curl -X POST http://kong:8001/consumers/{consumer}/plugins \
+ --data name=aws-lambda \
+ --data "config.aws_region=AWS_REGION" \
+ --data "config.function_name=LAMBDA_FUNCTION_NAME"
+```
+
+#### Without a database
+
+Configure this plugin on a [Consumer](https://docs.konghq.com/latest/admin-api/#Consumer-object) by adding this section do your declarative configuration file:
+
+```
+plugins:
+- name: aws-lambda
+ route: {route}
+ config:
+ aws_region: AWS_REGION
+ function_name: LAMBDA_FUNCTION_NAME
+```
-This plugin was developed against Kong `0.13`, and hence is compatible with
-Kong Enterprise `0.33`
+In both cases, `{consumer}` is the `id` or `username` of the Consumer that this plugin configuration will target.
+
+You can combine `consumer_id` and `service_id`
+
+In the same request, to furthermore narrow the scope of the plugin.
+
+### Global plugins
+
+- **Using a database**, all plugins can be configured using the `http://kong:8001/plugins/` endpoint.
+- **Without a database**, all plugins can be configured via the `plugins:` entry on the declarative configuration file.
+
+A plugin which is not associated to any Service, Route or Consumer (or API, if you are using an older version of Kong) is considered "global", and will be run on every request. Read the [Plugin Reference](https://docs.konghq.com/latest/admin-api/#add-plugin) and the [Plugin Precedence](https://docs.konghq.com/latest/admin-api/#precedence)sections for more information.
+
+## Parameters
+
+Here's a list of all the parameters which can be used in this plugin's configuration:
+
+| Form Parameter | default | description
+|----------------|---------|-------------
+| `name`|| The name of the plugin to use, in this case: `aws-lambda`.
+| `service_id`|| The id of the Service which this plugin will target.
+| `route_id` || The id of the Route which this plugin will target.
+| `enabled` | `true` | Whether this plugin will be applied.
+| `consumer_id` || The id of the Consumer which this plugin will target.
+|`config.aws_key`
*semi-optional* || The AWS key credential to be used when invoking the function. This value is required if `aws_secret` is defined.
+|`config.aws_secret`
*semi-optional* ||The AWS secret credential to be used when invoking the function. This value is required if `aws_key` is defined.
+|`config.aws_region` || The AWS region where the Lambda function is located. Regions supported are: `ap-northeast-1`, `ap-northeast-2`, `ap-south-1`, `ap-southeast-1`, `ap-southeast-2`, `ca-central-1`, `cn-north-1`, `cn-northwest-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `us-east-1`, `us-east-2`, `us-gov-west-1`, `us-west-1`, `us-west-2`.
+|`config.function_name` || The AWS Lambda function name to invoke.
+|`config.qualifier`
*optional* || The [`Qualifier`](http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax) to use when invoking the function.
+|`config.invocation_type`
*optional*| `RequestResponse` | The [`InvocationType`](http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax) to use when invoking the function. Available types are `RequestResponse`, `Event`, `DryRun`.
+|`config.log_type`
*optional* | `Tail`| The [`LogType`](http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax) to use when invoking the function. By default `None` and `Tail` are supported.
+|`config.port`
*optional* | `Tail`| The [`LogType`](http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax) to use when invoking the function. By default `None` and `Tail` are supported.
+|`config.timeout`| `60000` | An optional timeout in milliseconds when invoking the function.
+|`config.unhandled_status`
*optional* | `200`, `202` or `204` | The response status code to use (instead of the default `200`, `202`, or `204`) in the case of an [`Unhandled` Function Error](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_ResponseSyntax)
+|`config.forward_request_body`
*optional* | `false` | An optional value that defines whether the request body is to be sent in the `request_body` field of the JSON-encoded request. If the body arguments can be parsed, they will be sent in the separate `request_body_args` field of the request. The body arguments can be parsed for `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.
+|`config.forward_request_headers`
*optional* | `false` | An optional value that defines whether the original HTTP request headers are to be sent as a map in the `request_headers` field of the JSON-encoded request.
+|`config.forward_request_method`
*optional* | `false` | An optional value that defines whether the original HTTP request method verb is to be sent in the `request_method` field of the JSON-encoded request.
+|`config.forward_request_uri`
*optional* |`false`|An optional value that defines whether the original HTTP request URI is to be sent in the `request_uri` field of the JSON-encoded request. Request URI arguments (if any) will be sent in the separate `request_uri_args` field of the JSON body.
+|`config.is_proxy_integration`
*optional* | `false` | An optional value that defines whether the response format to receive from the Lambda to [this format](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format). Note that the parameter `isBase64Encoded` is not implemented.
+|`config.awsgateway_compatible`
*optional* | `false` | An optional value that defines whether the plugin should wrap requests into the Amazon API gateway.
+|`config.proxy_url`
*semi-optional* || An optional value that defines whether the plugin should connect through the given proxy server URL. This value is required if `proxy_scheme` is defined.
+|`config.proxy_scheme`
*semi-optional* || An optional value that defines which HTTP protocol scheme to use in order to connect through the proxy server. The schemes supported are: `http` and `https`. This value is required if `proxy_url` is defined.
+|`config.skip_large_bodies`
*optional* | `true` | An optional value that defines whether very large bodies (that are buffered to disk) should be sent by Kong. Note that sending very large bodies will have an impact on the system memory.
+
+## Notes
+
+When the IAM roles are used (default, if no `aws.key` / `aws.secret` is provided), the plugin will first try ECS metadata, and if not available it will fallback on EC2 metadata.
diff --git a/kong-plugin-liamp-0.1.0-1.rockspec b/kong-plugin-liamp-0.2.0-1.rockspec
similarity index 92%
rename from kong-plugin-liamp-0.1.0-1.rockspec
rename to kong-plugin-liamp-0.2.0-1.rockspec
index a65c8f6a5c7..a9aa52da49c 100644
--- a/kong-plugin-liamp-0.1.0-1.rockspec
+++ b/kong-plugin-liamp-0.2.0-1.rockspec
@@ -1,7 +1,7 @@
package = "kong-plugin-liamp" -- TODO: rename, must match the info in the filename of this rockspec!
-- as a convention; stick to the prefix: `kong-plugin-`
-version = "0.1.0-1" -- TODO: renumber, must match the info in the filename of this rockspec!
--- The version '0.1.0' is the source code version, the trailing '1' is the version of this rockspec.
+version = "0.2.0-1" -- TODO: renumber, must match the info in the filename of this rockspec!
+-- The version '0.2.0' is the source code version, the trailing '1' is the version of this rockspec.
-- whenever the source version changes, the rockspec should be reset to 1. The rockspec version is only
-- updated (incremented) when this file changes, but the source remains the same.
@@ -12,7 +12,7 @@ local pluginName = package:match("^kong%-plugin%-(.+)$") -- "myPlugin"
supported_platforms = {"linux", "macosx"}
source = {
url = "http://github.com/Tieske/kong-plugin-liamp.git",
- tag = "0.1.0"
+ tag = "0.2.0"
}
description = {
diff --git a/kong/plugins/liamp/handler.lua b/kong/plugins/liamp/handler.lua
index 8fd732e350a..6058bee8624 100644
--- a/kong/plugins/liamp/handler.lua
+++ b/kong/plugins/liamp/handler.lua
@@ -1,28 +1,19 @@
-- Copyright (C) Kong Inc.
--- Grab pluginname from module name
-local plugin_name = ({...})[1]:match("^kong%.plugins%.([^%.]+)")
-
-local BasePlugin = require "kong.plugins.base_plugin"
-local responses = require "kong.tools.responses"
-local utils = require "kong.tools.utils"
+local aws_v4 = require "kong.plugins.liamp.v4"
+local aws_serializer = require "kong.plugins.liamp.aws-serializer"
local http = require "resty.http"
local cjson = require "cjson.safe"
-local public_utils = require "kong.tools.public"
-local singletons = require "kong.singletons"
-local constants = require "kong.constants"
local meta = require "kong.meta"
-
-local aws_v4 = require("kong.plugins." .. plugin_name .. ".v4")
-local aws_serializer = require("kong.plugins." .. plugin_name .. ".aws-serializer")
+local constants = require "kong.constants"
local fetch_credentials
do
local credential_sources = {
- require("kong.plugins." .. plugin_name .. ".iam-ecs-credentials"),
+ require "kong.plugins.liamp.iam-ecs-credentials",
-- The EC2 one will always return `configured == true`, so must be the last!
- require("kong.plugins." .. plugin_name .. ".iam-ec2-credentials"),
+ require "kong.plugins.liamp.iam-ec2-credentials",
}
for _, credential_source in ipairs(credential_sources) do
@@ -36,37 +27,29 @@ end
local tostring = tostring
local tonumber = tonumber
-local pairs = pairs
local type = type
local fmt = string.format
-local ngx = ngx
-local ngx_req_read_body = ngx.req.read_body
-local ngx_req_get_uri_args = ngx.req.get_uri_args
-local ngx_req_get_headers = ngx.req.get_headers
local ngx_encode_base64 = ngx.encode_base64
local ngx_update_time = ngx.update_time
local ngx_now = ngx.now
-local IAM_CREDENTIALS_CACHE_KEY = "plugin." .. plugin_name .. ".iam_role_temp_creds"
-local LOG_PREFIX = "[" .. plugin_name .. "] "
-
-
-local new_tab
-do
- local ok
- ok, new_tab = pcall(require, "table.new")
- if not ok then
- new_tab = function(narr, nrec) return {} end
- end
-end
+local IAM_CREDENTIALS_CACHE_KEY = "plugin.liamp.iam_role_temp_creds"
local server_header_value
local server_header_name
-local response_bad_gateway
local AWS_PORT = 443
+local raw_content_types = {
+ ["text/plain"] = true,
+ ["text/html"] = true,
+ ["application/xml"] = true,
+ ["text/xml"] = true,
+ ["application/soap+xml"] = true,
+}
+
+
local function get_now()
ngx_update_time()
return ngx_now() * 1000 -- time is kept in seconds with millisecond resolution.
@@ -121,83 +104,34 @@ local function extract_proxy_response(content)
end
-local function send(status, content, headers)
- ngx.status = status
+local AWSLambdaHandler = {}
- if type(headers) == "table" then
- for k, v in pairs(headers) do
- ngx.header[k] = v
- end
- end
- if not ngx.header["Content-Length"] then
- ngx.header["Content-Length"] = #content
- end
+local function send(status, content, headers)
+ headers = kong.table.merge(headers) -- create a copy of headers
if server_header_value then
- ngx.header[server_header_name] = server_header_value
+ headers[server_header_name] = server_header_value
end
- ngx.print(content)
-
- return ngx.exit(status)
-end
-
-
-local function flush(ctx)
- ctx = ctx or ngx.ctx
- local response = ctx.delayed_response
- return send(response.status_code, response.content, response.headers)
-end
-
-
-local AWSLambdaHandler = BasePlugin:extend()
-
-
-function AWSLambdaHandler:new()
- AWSLambdaHandler.super.new(self, plugin_name)
+ return kong.response.exit(status, content, headers)
end
function AWSLambdaHandler:init_worker()
-
- if singletons.configuration.enabled_headers then
- -- newer `headers` config directive (0.14.x +)
- if singletons.configuration.enabled_headers[constants.HEADERS.VIA] then
- server_header_value = meta._SERVER_TOKENS
- server_header_name = constants.HEADERS.VIA
- else
- server_header_value = nil
- server_header_name = nil
- end
-
+ if kong.configuration.enabled_headers[constants.HEADERS.VIA] then
+ server_header_value = meta._SERVER_TOKENS
+ server_header_name = constants.HEADERS.VIA
else
- -- old `server_tokens` config directive (up to 0.13.x)
- if singletons.configuration.server_tokens then
- server_header_value = _KONG._NAME .. "/" .. _KONG._VERSION
- server_header_name = "Via"
- else
- server_header_value = nil
- server_header_name = nil
- end
- end
-
-
- -- response for BAD_GATEWAY was added in 0.14x
- response_bad_gateway = responses.send_HTTP_BAD_GATEWAY
- if not response_bad_gateway then
- response_bad_gateway = function(msg)
- ngx.log(ngx.ERR, LOG_PREFIX, msg)
- return responses.send(502, "Bad Gateway")
- end
+ server_header_value = nil
+ server_header_name = nil
end
end
function AWSLambdaHandler:access(conf)
- AWSLambdaHandler.super.access(self)
-
- local upstream_body = new_tab(0, 6)
+ local upstream_body = kong.table.new(0, 6)
+ local var = ngx.var
if conf.awsgateway_compatible then
upstream_body = aws_serializer(ngx.ctx, conf)
@@ -208,29 +142,36 @@ function AWSLambdaHandler:access(conf)
conf.forward_request_uri then
-- new behavior to forward request method, body, uri and their args
- local var = ngx.var
-
if conf.forward_request_method then
- upstream_body.request_method = var.request_method
+ upstream_body.request_method = kong.request.get_method()
end
if conf.forward_request_headers then
- upstream_body.request_headers = ngx_req_get_headers()
+ upstream_body.request_headers = kong.request.get_headers()
end
if conf.forward_request_uri then
- upstream_body.request_uri = var.request_uri
- upstream_body.request_uri_args = ngx_req_get_uri_args()
+ local path = kong.request.get_path()
+ local query = kong.request.get_raw_query()
+ if query ~= "" then
+ upstream_body.request_uri = path .. "?" .. query
+ else
+ upstream_body.request_uri = path
+ end
+ upstream_body.request_uri_args = kong.request.get_query()
end
if conf.forward_request_body then
- ngx_req_read_body()
-
- local body_args, err_code, body_raw = public_utils.get_body_info()
- if err_code == public_utils.req_body_errors.unknown_ct then
- -- don't know what this body MIME type is, base64 it just in case
- body_raw = ngx_encode_base64(body_raw)
- upstream_body.request_body_base64 = true
+ local content_type = kong.request.get_header("content-type")
+ local body_raw = kong.request.get_raw_body()
+ local body_args, err = kong.request.get_body()
+ if err and err:match("content type") then
+ body_args = {}
+ if not raw_content_types[content_type] then
+ -- don't know what this body MIME type is, base64 it just in case
+ body_raw = ngx_encode_base64(body_raw)
+ upstream_body.request_body_base64 = true
+ end
end
upstream_body.request_body = body_raw
@@ -240,16 +181,14 @@ function AWSLambdaHandler:access(conf)
else
-- backwards compatible upstream body for configurations not specifying
-- `forward_request_*` values
- ngx_req_read_body()
-
- local body_args = public_utils.get_body_args()
- upstream_body = utils.table_merge(ngx_req_get_uri_args(), body_args)
+ local body_args = kong.request.get_body()
+ upstream_body = kong.table.merge(kong.request.get_query(), body_args)
end
local upstream_body_json, err = cjson.encode(upstream_body)
if not upstream_body_json then
- ngx.log(ngx.ERR, LOG_PREFIX, "could not JSON encode upstream body",
- " to forward request values: ", err)
+ kong.log.err("could not JSON encode upstream body",
+ " to forward request values: ", err)
end
local host = fmt("lambda.%s.amazonaws.com", conf.aws_region)
@@ -275,16 +214,18 @@ function AWSLambdaHandler:access(conf)
query = conf.qualifier and "Qualifier=" .. conf.qualifier
}
- if (not conf.aws_key) or conf.aws_key == "" then
+ if not conf.aws_key then
-- no credentials provided, so try the IAM metadata service
- local iam_role_credentials, err = singletons.cache:get(
+ local iam_role_credentials, err = kong.cache:get(
IAM_CREDENTIALS_CACHE_KEY,
nil,
fetch_credentials
)
if not iam_role_credentials then
- return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
+ return kong.response.exit(500, {
+ message = "An unexpected error occurred"
+ })
end
opts.access_key = iam_role_credentials.access_key
@@ -299,7 +240,8 @@ function AWSLambdaHandler:access(conf)
local request
request, err = aws_v4(opts)
if err then
- return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
+ kong.log.err(err)
+ return kong.response.exit(500, { message = "An unexpected error occurred" })
end
-- Trigger request
@@ -315,12 +257,14 @@ function AWSLambdaHandler:access(conf)
ok, err = client:connect(host, port)
end
if not ok then
- return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
+ kong.log.err(err)
+ return kong.response.exit(500, { message = "An unexpected error occurred" })
end
ok, err = client:ssl_handshake()
if not ok then
- return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
+ kong.log.err(err)
+ return kong.response.exit(500, { message = "An unexpected error occurred" })
end
local res
@@ -331,7 +275,8 @@ function AWSLambdaHandler:access(conf)
headers = request.headers
}
if not res then
- return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
+ kong.log.err(err)
+ return kong.response.exit(500, { message = "An unexpected error occurred" })
end
local content = res:read_body()
@@ -341,25 +286,37 @@ function AWSLambdaHandler:access(conf)
ngx.ctx.KONG_WAITING_TIME = get_now() - kong_wait_time_start
local headers = res.headers
+ if var.http2 then
+ headers["Connection"] = nil
+ headers["Keep-Alive"] = nil
+ headers["Proxy-Connection"] = nil
+ headers["Upgrade"] = nil
+ headers["Transfer-Encoding"] = nil
+ end
+
if conf.proxy_url then
client:close()
else
ok, err = client:set_keepalive(conf.keepalive)
if not ok then
- return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
+ kong.log.err(err)
+ return kong.response.exit(500, { message = "An unexpected error occurred" })
end
end
local status
+
if conf.is_proxy_integration then
local proxy_response, err = extract_proxy_response(content)
if not proxy_response then
- return response_bad_gateway("could not JSON decode Lambda function " ..
- "response: " .. tostring(err))
+ kong.log.err(err)
+ return kong.response.exit(502, { message = "Bad Gateway",
+ error = "could not JSON decode Lambda " ..
+ "function response: " .. err })
end
status = proxy_response.status_code
- headers = utils.table_merge(headers, proxy_response.headers)
+ headers = kong.table.merge(headers, proxy_response.headers)
content = proxy_response.body
end
@@ -375,22 +332,10 @@ function AWSLambdaHandler:access(conf)
end
- local ctx = ngx.ctx
- if ctx.delay_response and not ctx.delayed_response then
- ctx.delayed_response = {
- status_code = status,
- content = content,
- headers = headers,
- }
-
- ctx.delayed_response_callback = flush
- return
- end
-
return send(status, content, headers)
end
AWSLambdaHandler.PRIORITY = 750
-AWSLambdaHandler.VERSION = "0.1.1"
+AWSLambdaHandler.VERSION = "0.2.0"
return AWSLambdaHandler
diff --git a/kong/plugins/liamp/schema.lua b/kong/plugins/liamp/schema.lua
index b12c82bade1..0c155ac2a72 100644
--- a/kong/plugins/liamp/schema.lua
+++ b/kong/plugins/liamp/schema.lua
@@ -1,139 +1,111 @@
-local Errors = require "kong.dao.errors"
+local typedefs = require "kong.db.schema.typedefs"
-local function check_status(status)
- if status and (status < 100 or status > 999) then
- return false, "unhandled_status must be within 100 - 999."
- end
-
- return true
-end
+local REGIONS = {
+ "ap-northeast-1", "ap-northeast-2",
+ "ap-south-1",
+ "ap-southeast-1", "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1", "eu-west-2",
+ "sa-east-1",
+ "us-east-1", "us-east-2",
+ "us-gov-west-1",
+ "us-west-1", "us-west-2",
+}
return {
+ name = "liamp",
fields = {
- timeout = {
- type = "number",
- default = 60000,
- required = true,
- },
- keepalive = {
- type = "number",
- default = 60000,
- required = true,
- },
- aws_key = {
- type = "string",
- },
- aws_secret = {
- type = "string",
- },
- aws_region = {
- type = "string",
- required = true,
- enum = {
- "us-east-1",
- "us-east-2",
- "us-west-1",
- "us-west-2",
- "ap-northeast-1",
- "ap-northeast-2",
- "ap-southeast-1",
- "ap-southeast-2",
- "ap-south-1",
- "ca-central-1",
- "eu-central-1",
- "eu-west-1",
- "eu-west-2",
- "sa-east-1",
- },
- },
- function_name = {
- type= "string",
- required = true,
- },
- qualifier = {
- type = "string",
- },
- invocation_type = {
- type = "string",
- required = true,
- default = "RequestResponse",
- enum = {
- "RequestResponse",
- "Event",
- "DryRun",
+ { run_on = typedefs.run_on_first },
+ { protocols = typedefs.protocols_http },
+ { config = {
+ type = "record",
+ fields = {
+ { timeout = {
+ type = "number",
+ required = true,
+ default = 60000,
+ } },
+ { keepalive = {
+ type = "number",
+ required = true,
+ default = 60000,
+ } },
+ { aws_key = {
+ type = "string",
+ } },
+ { aws_secret = {
+ type = "string",
+ } },
+ { aws_region = {
+ type = "string",
+ required = true,
+ one_of = REGIONS
+ } },
+ { function_name = {
+ type = "string",
+ required = true,
+ } },
+ { qualifier = {
+ type = "string",
+ } },
+ { invocation_type = {
+ type = "string",
+ required = true,
+ default = "RequestResponse",
+ one_of = { "RequestResponse", "Event", "DryRun" }
+ } },
+ { log_type = {
+ type = "string",
+ required = true,
+ default = "Tail",
+ one_of = { "Tail", "None" }
+ } },
+ { port = typedefs.port { default = 443 }, },
+ { unhandled_status = {
+ type = "integer",
+ between = { 100, 999 },
+ } },
+ { forward_request_method = {
+ type = "boolean",
+ default = false,
+ } },
+ { forward_request_uri = {
+ type = "boolean",
+ default = false,
+ } },
+ { forward_request_headers = {
+ type = "boolean",
+ default = false,
+ } },
+ { forward_request_body = {
+ type = "boolean",
+ default = false,
+ } },
+ { is_proxy_integration = {
+ type = "boolean",
+ default = false,
+ } },
+ { awsgateway_compatible = {
+ type = "boolean",
+ default = false,
+ } },
+ { proxy_scheme = {
+ type = "string",
+ one_of = { "http", "https" }
+ } },
+ { proxy_url = typedefs.url },
+ { skip_large_bodies = {
+ type = "boolean",
+ default = true,
+ } },
}
},
- log_type = {
- type = "string",
- required = true,
- default = "Tail",
- enum = {
- "Tail",
- "None",
- }
- },
- port = {
- type = "number",
- default = 443,
- },
- unhandled_status = {
- type = "number",
- func = check_status,
- },
- forward_request_method = {
- type = "boolean",
- default = false,
- },
- forward_request_uri = {
- type = "boolean",
- default = false,
- },
- forward_request_headers = {
- type = "boolean",
- default = false,
- },
- forward_request_body = {
- type = "boolean",
- default = false,
- },
- is_proxy_integration = {
- type = "boolean",
- default = false,
- },
- awsgateway_compatible = {
- type = "boolean",
- default = false,
- },
- proxy_scheme = {
- type = "string",
- enum = {
- "http",
- "https",
- }
- },
- proxy_url = {
- type = "string"
- },
- skip_large_bodies = {
- type = "boolean",
- default = true,
- },
- },
- self_check = function(schema, plugin_t, dao, is_update)
- if (plugin_t.aws_key or "") == "" then
- -- not provided
- if (plugin_t.aws_secret or "") ~= "" then
- return false, Errors.schema "You need to set both or neither of aws_key and aws_secret"
- end
- else
- -- provided
- if (plugin_t.aws_secret or "") == "" then
- return false, Errors.schema "You need to set both or neither of aws_key and aws_secret"
- end
- end
- if plugin_t.proxy_url and not plugin_t.proxy_scheme then
- return false, Errors.schema "You need to set proxy_scheme when proxy_url is set"
- end
- return true
- end
+ } },
+ entity_checks = {
+ { mutually_required = { "config.aws_key", "config.aws_secret" } },
+ { mutually_required = { "config.proxy_scheme", "config.proxy_url" } },
+ }
}
diff --git a/spec/plugins/liamp/02-schema_spec.lua b/spec/plugins/liamp/02-schema_spec.lua
index b977e09c6e7..312e4c55edf 100644
--- a/spec/plugins/liamp/02-schema_spec.lua
+++ b/spec/plugins/liamp/02-schema_spec.lua
@@ -1,81 +1,93 @@
-local aws_lambda_schema = require "kong.plugins.liamp.schema"
-local schemas = require "kong.dao.schemas_validation"
-local utils = require "kong.tools.utils"
-
-
-local validate_entity = schemas.validate_entity
+local schema_def = require "kong.plugins.liamp.schema"
+local v = require("spec.helpers").validate_plugin_config_schema
describe("Plugin: AWS Lambda (schema)", function()
- local DEFAULTS = {
- timeout = 60000,
- keepalive = 60000,
- aws_key = "my-key",
- aws_secret = "my-secret",
- aws_region = "us-east-1",
- function_name = "my-function",
- invocation_type = "RequestResponse",
- log_type = "Tail",
- port = 443,
- }
-
it("accepts nil Unhandled Response Status Code", function()
- local entity = utils.table_merge(DEFAULTS, { unhandled_status = nil })
- local ok, err = validate_entity(entity, aws_lambda_schema)
+ local ok, err = v({
+ unhandled_status = nil,
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
assert.is_nil(err)
- assert.True(ok)
+ assert.truthy(ok)
end)
it("accepts correct Unhandled Response Status Code", function()
- local entity = utils.table_merge(DEFAULTS, { unhandled_status = 412 })
- local ok, err = validate_entity(entity, aws_lambda_schema)
+ local ok, err = v({
+ unhandled_status = 412,
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
assert.is_nil(err)
- assert.True(ok)
+ assert.truthy(ok)
end)
it("errors with Unhandled Response Status Code less than 100", function()
- local entity = utils.table_merge(DEFAULTS, { unhandled_status = 99 })
- local ok, err = validate_entity(entity, aws_lambda_schema)
- assert.equal("unhandled_status must be within 100 - 999.", err.unhandled_status)
- assert.False(ok)
+ local ok, err = v({
+ unhandled_status = 99,
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
+ assert.equal("value should be between 100 and 999", err.config.unhandled_status)
+ assert.falsy(ok)
end)
it("errors with Unhandled Response Status Code greater than 999", function()
- local entity = utils.table_merge(DEFAULTS, { unhandled_status = 1000 })
- local ok, err = validate_entity(entity, aws_lambda_schema)
- assert.equal("unhandled_status must be within 100 - 999.", err.unhandled_status)
- assert.False(ok)
+ local ok, err = v({
+ unhandled_status = 1000,
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
+ assert.equal("value should be between 100 and 999", err.config.unhandled_status)
+ assert.falsy(ok)
end)
it("accepts with neither aws_key nor aws_secret", function()
- local entity = utils.table_merge(DEFAULTS, { aws_key = "", aws_secret = "" })
- local ok, err = validate_entity(entity, aws_lambda_schema)
+ local ok, err = v({
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
assert.is_nil(err)
- assert.True(ok)
+ assert.truthy(ok)
end)
it("errors with aws_secret but without aws_key", function()
- local entity = utils.table_merge(DEFAULTS, { aws_secret = "xx", aws_key = "" })
- local ok, err, self_err = validate_entity(entity, aws_lambda_schema)
- assert.is_nil(err)
- assert.equal("You need to set both or neither of aws_key and aws_secret", self_err.message)
- assert.False(ok)
+ local ok, err = v({
+ aws_secret = "xx",
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
+ assert.equal("all or none of these fields must be set: 'config.aws_key', 'config.aws_secret'", err["@entity"][1])
+ assert.falsy(ok)
end)
it("errors without aws_secret but with aws_key", function()
- local entity = utils.table_merge(DEFAULTS, { aws_secret = "", aws_key = "xx" })
- local ok, err, self_err = validate_entity(entity, aws_lambda_schema)
- assert.is_nil(err)
- assert.equal("You need to set both or neither of aws_key and aws_secret", self_err.message)
- assert.False(ok)
+ local ok, err = v({
+ aws_key = "xx",
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
+ assert.equal("all or none of these fields must be set: 'config.aws_key', 'config.aws_secret'", err["@entity"][1])
+ assert.falsy(ok)
end)
it("errors if proxy_scheme is missing while proxy_url is provided", function()
- local entity = utils.table_merge(DEFAULTS, { proxy_url = "http://hello.com/proxy" })
- local ok, err, self_err = validate_entity(entity, aws_lambda_schema)
- assert.is_nil(err)
- assert.equal("You need to set proxy_scheme when proxy_url is set", self_err.message)
- assert.False(ok)
+ local ok, err = v({
+ proxy_url = "http://hello.com/proxy",
+ aws_region = "us-east-1",
+ function_name = "my-function"
+ }, schema_def)
+
+ assert.equal("all or none of these fields must be set: 'config.proxy_scheme', 'config.proxy_url'", err["@entity"][1])
+ assert.falsy(ok)
end)
end)
diff --git a/spec/plugins/liamp/05-aws-serializer_spec.lua b/spec/plugins/liamp/05-aws-serializer_spec.lua
index 3aeea0d2f79..c95159a71fd 100644
--- a/spec/plugins/liamp/05-aws-serializer_spec.lua
+++ b/spec/plugins/liamp/05-aws-serializer_spec.lua
@@ -76,8 +76,8 @@ describe("[AWS Lambda] aws-gateway input", function()
pathParameters = {
version = "123",
},
- isBase64Encoded = false,
- body = "text",
+ isBase64Encoded = true,
+ body = ngx.encode_base64("text"),
headers = {
["multi-header"] = "first",
["single-header"] = "hello world",
@@ -129,8 +129,8 @@ describe("[AWS Lambda] aws-gateway input", function()
path = "/plain/strip/more",
resource = "/plain/strip",
pathParameters = {},
- isBase64Encoded = false,
- body = "text",
+ isBase64Encoded = true,
+ body = ngx.encode_base64("text"),
headers = {
["multi-header"] = "first",
["single-header"] = "hello world",
@@ -159,14 +159,14 @@ describe("[AWS Lambda] aws-gateway input", function()
description = "none",
ct = nil,
body_in = "text",
- body_out = "text",
- base64 = false,
+ body_out = ngx.encode_base64("text"),
+ base64 = true,
}, {
description = "application/json",
ct = "application/json",
body_in = [[{ "text": "some text" }]],
- body_out = [[{ "text": "some text" }]],
- base64 = false,
+ body_out = ngx.encode_base64([[{ "text": "some text" }]]),
+ base64 = true,
}, {
description = "unknown",
ct = "some-unknown-type-description",
diff --git a/spec/plugins/liamp/99-access_spec.lua b/spec/plugins/liamp/99-access_spec.lua
index f050810001a..b6738758f5b 100644
--- a/spec/plugins/liamp/99-access_spec.lua
+++ b/spec/plugins/liamp/99-access_spec.lua
@@ -6,13 +6,90 @@ local meta = require "kong.meta"
local server_tokens = meta._SERVER_TOKENS
+local fixtures = {
+ http_mock = {
+ lambda_plugin = [[
+
+ server {
+ server_name mock_aws_lambda;
+ listen 10001 ssl;
+
+ ssl_certificate ${{SSL_CERT}};
+ ssl_certificate_key ${{SSL_CERT_KEY}};
+ ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
+
+ location ~ "/2015-03-31/functions/(?:[^/])*/invocations" {
+ content_by_lua_block {
+ local function x()
+ local function say(res, status)
+ ngx.header["x-amzn-RequestId"] = "foo"
+
+ if string.match(ngx.var.uri, "functionWithUnhandledError") then
+ ngx.header["X-Amz-Function-Error"] = "Unhandled"
+ end
+
+ ngx.status = status
+
+ if string.match(ngx.var.uri, "functionWithBadJSON") then
+ local badRes = "{\"foo\":\"bar\""
+ ngx.header["Content-Length"] = #badRes + 1
+ ngx.say(badRes)
+
+ elseif string.match(ngx.var.uri, "functionWithNoResponse") then
+ ngx.header["Content-Length"] = 0
+
+ elseif type(res) == 'string' then
+ ngx.header["Content-Length"] = #res + 1
+ ngx.say(res)
+
+ else
+ ngx.req.discard_body()
+ ngx.header['Content-Length'] = 0
+ end
+
+ ngx.exit(0)
+ end
+
+ ngx.sleep(.2) -- mock some network latency
+
+ local invocation_type = ngx.var.http_x_amz_invocation_type
+ if invocation_type == 'Event' then
+ say(nil, 202)
+
+ elseif invocation_type == 'DryRun' then
+ say(nil, 204)
+ end
+
+ local qargs = ngx.req.get_uri_args()
+ ngx.req.read_body()
+ local args = require("cjson").decode(ngx.req.get_body_data())
+
+ say(ngx.req.get_body_data(), 200)
+ end
+ local ok, err = pcall(x)
+ if not ok then
+ ngx.log(ngx.ERR, "Mock error: ", err)
+ end
+ }
+ }
+ }
+
+ ]]
+ },
+}
+
+
for _, strategy in helpers.each_strategy() do
describe("Plugin: AWS Lambda (access) [#" .. strategy .. "]", function()
local proxy_client
local admin_client
- setup(function()
- local bp = helpers.get_db_utils(strategy)
+ lazy_setup(function()
+ local bp = helpers.get_db_utils(strategy, {
+ "routes",
+ "services",
+ "plugins",
+ }, { "liamp" })
local route1 = bp.routes:insert {
hosts = { "lambda.com" },
@@ -98,9 +175,15 @@ for _, strategy in helpers.each_strategy() do
service = service12,
}
+ local route14 = bp.routes:insert {
+ hosts = { "lambda14.com" },
+ protocols = { "http", "https" },
+ service = ngx.null,
+ }
+
bp.plugins:insert {
name = "liamp",
- route_id = route1.id,
+ route = { id = route1.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -112,7 +195,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route2.id,
+ route = { id = route2.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -125,7 +208,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route3.id,
+ route = { id = route3.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -138,7 +221,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route4.id,
+ route = { id = route4.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -151,7 +234,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route5.id,
+ route = { id = route5.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -163,7 +246,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route6.id,
+ route = { id = route6.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -176,7 +259,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route7.id,
+ route = { id = route7.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -189,7 +272,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route8.id,
+ route = { id = route8.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -202,7 +285,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route9.id,
+ route = { id = route9.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -218,7 +301,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route10.id,
+ route = { id = route10.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -234,7 +317,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route11.id,
+ route = { id = route11.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -247,7 +330,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route12.id,
+ route = { id = route12.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -260,7 +343,7 @@ for _, strategy in helpers.each_strategy() do
bp.plugins:insert {
name = "liamp",
- route_id = route13.id,
+ route = { id = route13.id },
config = {
port = 10001,
aws_key = "mock-key",
@@ -271,11 +354,23 @@ for _, strategy in helpers.each_strategy() do
}
}
- assert(helpers.start_kong{
+ bp.plugins:insert {
+ name = "liamp",
+ route = { id = route14.id },
+ config = {
+ port = 10001,
+ aws_key = "mock-key",
+ aws_secret = "mock-secret",
+ aws_region = "us-east-1",
+ function_name = "kongLambdaTest",
+ },
+ }
+
+ assert(helpers.start_kong({
database = strategy,
- custom_plugins = "liamp",
+ plugins = "liamp",
nginx_conf = "spec/fixtures/custom_nginx.template",
- })
+ }, nil, nil, fixtures))
end)
before_each(function()
@@ -288,7 +383,7 @@ for _, strategy in helpers.each_strategy() do
admin_client:close()
end)
- teardown(function()
+ lazy_teardown(function()
helpers.stop_kong()
end)
@@ -344,6 +439,19 @@ for _, strategy in helpers.each_strategy() do
assert.is_string(res.headers["x-amzn-RequestId"])
assert.equal("some_value_json1", body.key1)
end)
+ it("passes empty json arrays unmodified", function()
+ local res = assert(proxy_client:send {
+ method = "POST",
+ path = "/post",
+ headers = {
+ ["Host"] = "lambda.com",
+ ["Content-Type"] = "application/json"
+ },
+ body = '[{}, []]'
+ })
+ assert.res_status(200, res)
+ assert.equal('[{},[]]', string.gsub(res:read_body(), "\n",""))
+ end)
it("invokes a Lambda function with POST and both querystring and body params", function()
local res = assert(proxy_client:send {
method = "POST",
@@ -584,11 +692,7 @@ for _, strategy in helpers.each_strategy() do
})
if server_tokens then
- -- post-0.14
assert.equal(server_tokens, res.headers["Via"])
- else
- -- pre-0.14
- assert.equal("kong/", res.headers["Via"]:sub(1,5))
end
end)
@@ -764,13 +868,24 @@ for _, strategy in helpers.each_strategy() do
})
assert.res_status(502, res)
-
- local b = assert.response(res).has.jsonbody()
+ local b = assert.response(res).has.jsonbody()
assert.equal("Bad Gateway", b.message)
end)
+ it("invokes a Lambda function with GET using serviceless route", function()
+ local res = assert(proxy_client:send {
+ method = "GET",
+ path = "/get?key1=some_value1&key2=some_value2&key3=some_value3",
+ headers = {
+ ["Host"] = "lambda14.com"
+ }
+ })
+ assert.res_status(200, res)
+ local body = assert.response(res).has.jsonbody()
+ assert.is_string(res.headers["x-amzn-RequestId"])
+ assert.equal("some_value1", body.key1)
+ assert.is_nil(res.headers["X-Amz-Function-Error"])
+ end)
end)
-
end)
end
-