Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Liquid context debugging policy #849

Merged
merged 8 commits into from
Aug 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory we could add __pairs metamethod to the linked list that understands this.
http://lua-users.org/wiki/GeneralizedPairsAndIpairs

Then you could just iterate through the context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea, but I'd rather leave it for a future PR as it involves adding the functionality in the LinkedList module, test it, and check whether we can use it in some of the existing callers.

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]