diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7260fc9..5b98f6943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/gateway/src/apicast/policy/liquid_context_debug/apicast-policy.json b/gateway/src/apicast/policy/liquid_context_debug/apicast-policy.json new file mode 100644 index 000000000..90c89f13a --- /dev/null +++ b/gateway/src/apicast/policy/liquid_context_debug/apicast-policy.json @@ -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": { + } + } +} diff --git a/gateway/src/apicast/policy/liquid_context_debug/context_content.lua b/gateway/src/apicast/policy/liquid_context_debug/context_content.lua new file mode 100644 index 000000000..5da273836 --- /dev/null +++ b/gateway/src/apicast/policy/liquid_context_debug/context_content.lua @@ -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 diff --git a/gateway/src/apicast/policy/liquid_context_debug/init.lua b/gateway/src/apicast/policy/liquid_context_debug/init.lua new file mode 100644 index 000000000..765265854 --- /dev/null +++ b/gateway/src/apicast/policy/liquid_context_debug/init.lua @@ -0,0 +1 @@ +return require('liquid_context_debug') diff --git a/gateway/src/apicast/policy/liquid_context_debug/liquid_context_debug.lua b/gateway/src/apicast/policy/liquid_context_debug/liquid_context_debug.lua new file mode 100644 index 000000000..07cbc23aa --- /dev/null +++ b/gateway/src/apicast/policy/liquid_context_debug/liquid_context_debug.lua @@ -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 diff --git a/gateway/src/apicast/policy/ngx_variable.lua b/gateway/src/apicast/policy/ngx_variable.lua index e5745000c..275f81fe2 100644 --- a/gateway/src/apicast/policy/ngx_variable.lua +++ b/gateway/src/apicast/policy/ngx_variable.lua @@ -1,3 +1,5 @@ +local LinkedList = require('apicast.linked_list') + local _M = {} local function context_values() @@ -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 diff --git a/spec/policy/liquid_context_debug/context_content_spec.lua b/spec/policy/liquid_context_debug/context_content_spec.lua new file mode 100644 index 000000000..dbfcff2c2 --- /dev/null +++ b/spec/policy/liquid_context_debug/context_content_spec.lua @@ -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) diff --git a/spec/policy/liquid_context_debug/liquid_context_debug_spec.lua b/spec/policy/liquid_context_debug/liquid_context_debug_spec.lua new file mode 100644 index 000000000..204cd6a66 --- /dev/null +++ b/spec/policy/liquid_context_debug/liquid_context_debug_spec.lua @@ -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) diff --git a/spec/policy/ngx_variable_spec.lua b/spec/policy/ngx_variable_spec.lua index 2faaa5e04..b93231f55 100644 --- a/spec/policy/ngx_variable_spec.lua +++ b/spec/policy/ngx_variable_spec.lua @@ -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) diff --git a/t/apicast-policy-liquid-context-debug.t b/t/apicast-policy-liquid-context-debug.t new file mode 100644 index 000000000..24356c146 --- /dev/null +++ b/t/apicast-policy-liquid-context-debug.t @@ -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]