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

[config] add support to filter services by endpoint. #1022

Merged
merged 3 commits into from Apr 30, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Ability to configure client certificate chain depth [PR #1006](https://github.com/3scale/APIcast/pull/1006)
- You can filter services by endpoint name using Regexp [PR #1022](https://github.com/3scale/APIcast/pull/1022) [THREESCALE-1524](https://issues.jboss.org/browse/THREESCALE-1524)

### Fixed

Expand Down
23 changes: 23 additions & 0 deletions doc/parameters.md
Expand Up @@ -186,6 +186,29 @@ before the client is throttled by adding latency.
When set to _true_, APIcast will log the response code of the response returned by the API backend in 3scale. In some plans this information can later be consulted from the 3scale admin portal.
Find more information about the Response Codes feature on the [3scale support site](https://access.redhat.com/documentation/en-us/red_hat_3scale/2.saas/html/analytics/response-codes-tracking).

### `APICAST_SERVICES_FILTER_BY_URL`
**Value:** a PCRE (Perl Compatible Regular Expression)
**Example:** .*.example.com

Used to filter the service configured in the 3scale API Manager, the filter
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably should mention the filter is PCRE.

matches with the public base URL. Services that do not match the filter will be
discarded. If the regular expression cannot be compiled no services will be
loaded.

Note: If a service does not match, but is included in the
`APICAST_SERVICES_LIST`, service will not be discarded

Example:

Regexp Filter: http:\/\/.*.google.com
Service 1: backend endpoint http://www.google.com
Service 2: backend endpoint http://www.yahoo.com
Service 3: backend endpoint http://mail.google.com
Service 4: backend endpoint http://mail.yahoo.com

The services that will be configured in Apicast will be 1 and 3. Services 2 and
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
4 will be discarded.

### `APICAST_SERVICES_LIST`
**Value:** a comma-separated list of service IDs

Expand Down
32 changes: 23 additions & 9 deletions gateway/src/apicast/configuration.lua
Expand Up @@ -12,14 +12,16 @@ local insert = table.insert
local setmetatable = setmetatable
local null = ngx.null

local re = require 'ngx.re'
local env = require 'resty.env'
local resty_url = require 'resty.url'
local util = require 'apicast.util'
local policy_chain = require 'apicast.policy_chain'
local mapping_rule = require 'apicast.mapping_rule'
local tab_new = require('resty.core.base').new_tab

local re = require 'ngx.re'
local match = ngx.re.match

local mt = { __index = _M, __tostring = function() return 'Configuration' end }

local function map(func, tbl)
Expand Down Expand Up @@ -83,7 +85,6 @@ function _M.parse_service(service)
local proxy = service.proxy or empty_t
local backend = backend_endpoint(proxy)


return Service.new({
id = tostring(service.id or 'default'),
backend_version = backend_version,
Expand Down Expand Up @@ -140,21 +141,34 @@ function _M.services_limit()
end

function _M.filter_services(services, subset)
subset = subset and util.to_hash(subset) or _M.services_limit()
if not subset or not next(subset) then return services end
local selected_services = {}
local service_regexp_filter = env.value("APICAST_SERVICES_FILTER_BY_URL")
eloycoto marked this conversation as resolved.
Show resolved Hide resolved

if service_regexp_filter then
-- Checking that the regexp sent is correct, if not an empty service list
-- will be returned.
local _, err = match("", service_regexp_filter, 'oj')
if err then
-- @todo this return and empty list, Apicast will continue running maybe
-- process need to be stopped here.
ngx.log(ngx.ERR, "APICAST_SERVICES_FILTER_BY_URL cannot compile and all services are filtering out, error: ", err)
return selected_services
end
end

local s = {}
subset = subset and util.to_hash(subset) or _M.services_limit()
if (not subset or not next(subset)) and not service_regexp_filter then return services end
subset = subset or {}

for i = 1, #services do
local service = services[i]
if subset[service.id] then
insert(s, service)
if service:match_host(service_regexp_filter) or subset[service.id] then
insert(selected_services, service)
else
ngx.log(ngx.WARN, 'filtering out service ', service.id)
end
end

return s
return selected_services
end

function _M.new(configuration)
Expand Down
20 changes: 20 additions & 0 deletions gateway/src/apicast/configuration/service.lua
Expand Up @@ -8,7 +8,9 @@ local rawget = rawget
local lower = string.lower
local gsub = string.gsub
local select = select

local re = require 'ngx.re'
local match = ngx.re.match

local http_authorization = require 'resty.http_authorization'

Expand Down Expand Up @@ -264,4 +266,22 @@ function _M:get_usage(method, path)
end
end

--- Validate that the given regexp match with one of the service hosts.
--- This function needs a valid regexp, if not will return false
-- @tparam string regexp Regular expresion to match with the host
-- @return bool true if match
function _M:match_host(regexp)
if not regexp or not self.hosts then
return false
end

for j = 1, #self.hosts do
local val, _ = match(self.hosts[j], regexp, 'oj')
if val then
return true
end
end
return false
end

return _M
60 changes: 56 additions & 4 deletions spec/configuration_spec.lua
Expand Up @@ -25,7 +25,6 @@ describe('Configuration object', function()
assert.same('example.com', config.hostname_rewrite)
end)


it('has a default message, content-type, and status for the auth failed error', function()
local config = configuration.parse_service({})

Expand Down Expand Up @@ -112,19 +111,72 @@ describe('Configuration object', function()
end)

describe('.filter_services', function()
local Service = require 'apicast.configuration.service'
local filter_services = configuration.filter_services

it('works with nil', function()
local services = { { id = '42' } }
local services = { Service.new({id="42"})}
assert.equal(services, filter_services(services))
end)

it('works with table with ids', function()
local services = { { id = '42' } }

local services = { Service.new({id="42"})}
assert.same(services, filter_services(services, { '42' }))
assert.same({}, filter_services(services, { '21' }))
end)

describe("with service filter", function()
eloycoto marked this conversation as resolved.
Show resolved Hide resolved

local mockservices = {
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
Service.new({id="42", hosts={"test.foo.com", "test.bar.com"}}),
Service.new({id="12", hosts={"staging.foo.com"}}),
Service.new({id="21", hosts={"prod.foo.com"}}),
}

it("with empty env variable", function()
env.set('APICAST_SERVICES_FILTER_BY_URL', '')
assert.same(filter_services(mockservices, nil), mockservices)
end)

it("it does not discard any service when there is not regex", function()
assert.same(filter_services(mockservices, nil), mockservices)
end)

it("reads from environment variable", function()
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
env.set('APICAST_SERVICES_FILTER_BY_URL', '.*.foo.com')
assert.same(filter_services(mockservices, nil), mockservices)

env.set('APICAST_SERVICES_FILTER_BY_URL', '^test.*')
assert.same(filter_services(mockservices, nil), {mockservices[1]})

env.set('APICAST_SERVICES_FILTER_BY_URL', '^(test|prod).*')
assert.same(filter_services(mockservices, nil), {mockservices[1], mockservices[3]})
end)

it("matches the second host", function()
env.set('APICAST_SERVICES_FILTER_BY_URL', '^test.bar.com')
assert.same(filter_services(mockservices, nil), {mockservices[1]})
end)

it("validates invalid regexp", function()
env.set('APICAST_SERVICES_FILTER_BY_URL', '^]')
assert.same(filter_services(mockservices, nil), {})
end)

it("combination with service list", function()
env.set('APICAST_SERVICES_FILTER_BY_URL', '^test.*')
env.set('APICAST_SERVICES_LIST', '42,21')

assert.same(filter_services(mockservices, {"21"}), {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this test be clearer if the 2 variables were set at the beginning and we had a single assert?

mockservices[1],
mockservices[3]})

assert.same(filter_services(mockservices, nil), {
mockservices[1],
mockservices[3]})
end)
end)

end)

insulate('.services_limit', function()
Expand Down