Skip to content

Commit

Permalink
Merge pull request #849 from 3scale/liquid-context-policy
Browse files Browse the repository at this point in the history
Liquid context debugging policy
  • Loading branch information
davidor authored Aug 27, 2018
2 parents bf51867 + b8965b9 commit c3ec9d1
Show file tree
Hide file tree
Showing 10 changed files with 299 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Support for HTTP Proxy [THREESCALE-221](https://issues.jboss.org/browse/THREESCALE-221), [#709](https://github.com/3scale/apicast/issues/709)
- Conditions for the limits of the rate-limit policy [PR #839](https://github.com/3scale/apicast/pull/839)
- `bin/apicast console` to start Lua REPL with APIcast code loaded [PR #853](https://github.com/3scale/apicast/pull/853)
- Liquid Context Debugging policy. It's a policy only meant for debugging purposes, returns the context available when evaluating liquid [PR #849](https://github.com/3scale/apicast/pull/849)

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "http://apicast.io/policy-v1/schema#manifest#",
"name": "Liquid context debug",
"summary": "Inspects the available liquid context.",
"description": [
"This is a policy meant only for debugging purposes. This policy ",
"returns the context available when evaluating liquid. Any policy can ",
"modify the context that is shared between policies and that context is ",
"available when evaluating liquid. However, documenting what's available ",
"is not possible because policies can add any arbitrary field. Users who ",
"want to develop a policy can use this one to know the context available ",
"in their configuration."
],
"version": "builtin",
"configuration": {
"type": "object",
"properties": {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
local type = type
local pairs = pairs
local tostring = tostring

local _M = {}

-- The context usually contains a lot of information. For example, it includes
-- the whole service configuration. Also, some of the values are objects that
-- we can't really use when evaluating liquid, like functions.
-- That's why we define only a few types of keys and values to return.
local accepted_types_for_keys = {
string = true,
number = true,
}

local accepted_types_for_values = {
string = true,
number = true,
table = true
}

-- In the context, there might be large integers as keys. This can be a problem
-- when converting to JSON. Imagine an entry of the table like this:
-- { [10000] = 'something' }. To convert that to JSON, we need an array of 1000
-- positions with only one occupied. With arrays like these, 'cjson' raises an
-- "Excessively sparse arrays" error:
-- https://github.com/efelix/lua-cjson/blob/4f27182acabc435fcc220fdb710ddfaf4e648c86/README#L140
-- For example, there is a lrucache instance with a table that maps service ids
-- (numbers) to hosts. So we can have something like { [123456] = "127.0.0.1" }.
-- This can be limited using a global setting of the 'cjson' library. However,
-- we're going to implement an add-hoc solution here so we don't affect the
-- other modules. We're going to convert those large ints to strings as a
-- workaround.
local max_integer_key = 1000

local function key(k)
if type(k) ~= 'number' then return k end

if k <= max_integer_key then
return k
else
return tostring(k)
end
end

local value_from

local ident = function(...) return ... end
local value_from_fun = {
string = ident, number = ident,
table = function(table)
local res = {}
for k, v in pairs(table) do
local wanted_types = accepted_types_for_keys[type(k)] and
accepted_types_for_values[type(v)]

if wanted_types then
res[key(k)] = value_from(v)
end
end

return res
end
}

value_from = function(object)
local fun = value_from_fun[type(object)]
if fun then return fun(object) end
end

local function add_content(object, acc)
if type(object) ~= 'table' then return nil end

-- The context is a list where each element has a "current" and a "next".
local current = object.current
local next = object.next

local values_of_current = value_from(current or object)

for k, v in pairs(values_of_current) do
if acc[key(k)] == nil then -- to return only the first occurrence
acc[key(k)] = v
end
end

if next then
add_content(next, acc)
end
end

function _M.from(context)
local res = {}
add_content(context, res)
return res
end

return _M
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/liquid_context_debug/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('liquid_context_debug')
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
local context_content = require('context_content')
local cjson = require('cjson')
local policy = require('apicast.policy')
local ngx_variable = require('apicast.policy.ngx_variable')
local _M = policy.new('Liquid context debug')

local new = _M.new

function _M.new(config)
local self = new(config)
return self
end

function _M.content(_, context)
local liquid_context = ngx_variable.available_context(context)
local content = context_content.from(liquid_context)

ngx.say(cjson.encode(content))
end

return _M
4 changes: 3 additions & 1 deletion gateway/src/apicast/policy/ngx_variable.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local LinkedList = require('apicast.linked_list')

local _M = {}

local function context_values()
Expand All @@ -10,7 +12,7 @@ local function context_values()
end

function _M.available_context(policies_context)
return setmetatable(context_values(), { __index = policies_context })
return LinkedList.readonly(context_values(), policies_context)
end

return _M
74 changes: 74 additions & 0 deletions spec/policy/liquid_context_debug/context_content_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
local LinkedList = require 'apicast.linked_list'
local context_content = require 'apicast.policy.liquid_context_debug.context_content'

describe('Context content', function()
describe('.from', function()
it('returns the content from a context with one node', function()
local t = { a = 1, b = 2 }
local context = LinkedList.readonly(t)

local content = context_content.from(context)

assert.contains(t, content)
end)

it('returns the content from a context with several nodes', function()
local context = LinkedList.readonly(
{ a = 1 },
LinkedList.readonly(
{ b = 2, c = 3 },
LinkedList.readonly(
{ d = 4 },
{ e = 5, f = 6 }
)
)
)

local content = context_content.from(context)

local expected = { a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 }
assert.contains(expected, content)
end)

it('returns only the first value of a repeated element', function()
local context = LinkedList.readonly(
{ a = 1 },
LinkedList.readonly(
{ a = 2 },
LinkedList.readonly(
{ a = 3 },
{ a = 4 }
)
)
)

local content = context_content.from(context)

assert.equals(1, content.a)
end)

it('ignores keys that are not strings or numbers', function()
local context = LinkedList.readonly(
{ a = 1, [function() end] = 2, [{}] = 3 }
)

local content = context_content.from(context)

assert.contains({ a = 1 }, content)
end)

it('ignores values that are not strings, numbers or tables', function()
local context = LinkedList.readonly(
{ a = 1, b = {}, c = function() end }
)

local content = context_content.from(context)

assert.contains({ a = 1, b = {} }, content)
end)

it('returns empty if the context is empty', function()
assert.same({}, context_content.from({}))
end)
end)
end)
32 changes: 32 additions & 0 deletions spec/policy/liquid_context_debug/liquid_context_debug_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local ngx_variable = require('apicast.policy.ngx_variable')
local LinkedList = require('apicast.linked_list')
local context_content = require('apicast.policy.liquid_context_debug.context_content')
local cjson = require('cjson')

local LiquidContextDebug = require 'apicast.policy.liquid_context_debug'

describe('Liquid context debug policy', function()
describe('.content', function()
before_each(function()
stub(ngx, 'say')
end)

it('calls ngx.say with the "ngx_variable" available context in JSON', function()
-- Return something simple with just headers instead of mocking all the
-- ngx.var.* needed.
stub(ngx_variable, 'available_context', function(policies_context)
return LinkedList.readonly({ headers = { some_header = 'some_val' } },
policies_context)
end)
local policies_context = { a = 1 }

LiquidContextDebug.new():content(policies_context)

local expected_content = context_content.from(
ngx_variable.available_context(policies_context)
)
local expected_json = cjson.encode(expected_content)
assert.stub(ngx.say).was_called_with(expected_json)
end)
end)
end)
18 changes: 17 additions & 1 deletion spec/policy/ngx_variable_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ describe('ngx_variable', function()

local context = ngx_variable.available_context()

assert('some_val', context.headers['some_header'])
assert.equals('some_val', context.headers['some_header'])
end)

it('gives precedence to what is exposed in "ngx_variable"', function()
local headers_in_ngx_variable = { some_header = 'some_val' }
local headers_in_context = { some_header = 'different_val' }

stub(ngx.req, 'get_headers', function()
return headers_in_ngx_variable
end)

local policies_context = { headers = headers_in_context }

local liquid_context = ngx_variable.available_context(policies_context)

assert.equals(headers_in_ngx_variable.some_header,
liquid_context.headers.some_header)
end)
end)
end)
33 changes: 33 additions & 0 deletions t/apicast-policy-liquid-context-debug.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use lib 't';
use Test::APIcast::Blackbox 'no_plan';

run_tests();

__DATA__
=== TEST 1: liquid_context_debug policy does not crash
If there's a problem while parsing the context or converting it to JSON, this
will crash.
--- configuration
{
"services": [
{
"id": 42,
"proxy": {
"policy_chain": [
{
"name": "liquid_context_debug",
"configuration": {}
}
],
"proxy_rules": [
]
}
}
]
}
--- request
GET /
--- error_code: 200
--- no_error_log
[error]

0 comments on commit c3ec9d1

Please sign in to comment.