663 lines
18 KiB
Lua
663 lines
18 KiB
Lua
local cjson = require "cjson"
|
|
local declarative = require "kong.db.declarative"
|
|
local helpers = require "spec.helpers"
|
|
local format_host = require("kong.tools.ip").format_host
|
|
local kong_table = require "kong.tools.table"
|
|
local https_server = require "spec.fixtures.https_server"
|
|
local uuid = require("kong.tools.uuid").uuid
|
|
|
|
|
|
local CONSISTENCY_FREQ = 1
|
|
local HEALTHCHECK_INTERVAL = 1
|
|
local SLOTS = 10
|
|
local TEST_LOG = false -- extra verbose logging
|
|
local healthchecks_defaults = {
|
|
active = {
|
|
timeout = 1,
|
|
concurrency = 10,
|
|
http_path = "/",
|
|
healthy = {
|
|
interval = 0, -- 0 = disabled by default
|
|
http_statuses = { 200, 302 },
|
|
successes = 2,
|
|
},
|
|
unhealthy = {
|
|
interval = 0, -- 0 = disabled by default
|
|
http_statuses = { 429, 404,
|
|
500, 501, 502, 503, 504, 505 },
|
|
tcp_failures = 2,
|
|
timeouts = 3,
|
|
http_failures = 5,
|
|
},
|
|
},
|
|
passive = {
|
|
healthy = {
|
|
http_statuses = { 200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
|
|
300, 301, 302, 303, 304, 305, 306, 307, 308 },
|
|
successes = 5,
|
|
},
|
|
unhealthy = {
|
|
http_statuses = { 429, 500, 503 },
|
|
tcp_failures = 2,
|
|
timeouts = 7,
|
|
http_failures = 5,
|
|
},
|
|
},
|
|
}
|
|
local get_available_port = helpers.get_available_port
|
|
|
|
|
|
local prefix = ""
|
|
|
|
|
|
local function healthchecks_config(config)
|
|
return kong_table.cycle_aware_deep_merge(healthchecks_defaults, config)
|
|
end
|
|
|
|
|
|
local function direct_request(host, port, path, protocol, host_header)
|
|
local pok, client = pcall(helpers.http_client, {
|
|
host = host,
|
|
port = port,
|
|
scheme = protocol,
|
|
})
|
|
if not pok then
|
|
return nil, "pcall: " .. client .. " : " .. host ..":"..port
|
|
end
|
|
if not client then
|
|
return nil, "client"
|
|
end
|
|
|
|
local res, err = client:send {
|
|
method = "GET",
|
|
path = path,
|
|
headers = { ["Host"] = host_header or host }
|
|
}
|
|
local body = res and res:read_body()
|
|
client:close()
|
|
if err then
|
|
return nil, err
|
|
end
|
|
return body
|
|
end
|
|
|
|
|
|
local function put_target_endpoint(upstream_id, host, port, endpoint)
|
|
if host == "[::1]" then
|
|
host = "[0000:0000:0000:0000:0000:0000:0000:0001]"
|
|
end
|
|
local path = "/upstreams/" .. upstream_id
|
|
.. "/targets/"
|
|
.. format_host(host, port)
|
|
.. "/" .. endpoint
|
|
local api_client = helpers.admin_client()
|
|
local res, err = assert(api_client:put(prefix .. path, {
|
|
headers = {
|
|
["Content-Type"] = "application/json",
|
|
},
|
|
body = {},
|
|
}))
|
|
api_client:close()
|
|
return res, err
|
|
end
|
|
|
|
-- client_sync_request requires a route with
|
|
-- hosts = { "200.test" } to sync requests
|
|
local function client_sync_request(proxy_host , proxy_port)
|
|
-- kong have two port 9100(TCP) and 80(HTTP)
|
|
-- we just need to request http
|
|
if proxy_port == 9100 then
|
|
proxy_port = 80
|
|
end
|
|
local proxy_client = helpers.proxy_client({
|
|
host = proxy_host,
|
|
port = proxy_port,
|
|
})
|
|
|
|
local res = assert(proxy_client:send {
|
|
method = "GET",
|
|
headers = {
|
|
["Host"] = "200.test",
|
|
},
|
|
path = "/",
|
|
})
|
|
local status = res.status
|
|
proxy_client:close()
|
|
return status == 200
|
|
end
|
|
|
|
local function client_requests(n, host_or_headers, proxy_host, proxy_port, protocol, uri)
|
|
local oks, fails = 0, 0
|
|
local last_status
|
|
for _ = 1, n do
|
|
-- hack sync avoid concurrency request
|
|
-- There is an issue here, if a request is completed and a response is received,
|
|
-- it does not necessarily mean that the log phase has been executed
|
|
-- (many operations require execution in the log phase, such as passive health checks),
|
|
-- so we need to ensure that the log phase has been completely executed here.
|
|
-- We choose to wait here for the log phase of the last connection to finish.
|
|
client_sync_request(proxy_host, proxy_port)
|
|
local client
|
|
if proxy_host and proxy_port then
|
|
client = helpers.http_client({
|
|
host = proxy_host,
|
|
port = proxy_port,
|
|
scheme = protocol,
|
|
})
|
|
|
|
else
|
|
if protocol == "https" then
|
|
client = helpers.proxy_ssl_client()
|
|
else
|
|
client = helpers.proxy_client()
|
|
end
|
|
end
|
|
|
|
local res = client:send {
|
|
method = "GET",
|
|
path = uri or "/",
|
|
headers = type(host_or_headers) == "string"
|
|
and { ["Host"] = host_or_headers }
|
|
or host_or_headers
|
|
or {}
|
|
}
|
|
if not res then
|
|
fails = fails + 1
|
|
if TEST_LOG then
|
|
print("FAIL (no body)")
|
|
end
|
|
elseif res.status == 200 then
|
|
oks = oks + 1
|
|
if TEST_LOG then
|
|
print("OK ", res.status, res:read_body())
|
|
end
|
|
elseif res.status > 399 then
|
|
fails = fails + 1
|
|
if TEST_LOG then
|
|
print("FAIL ", res.status, res:read_body())
|
|
end
|
|
end
|
|
last_status = res and res.status
|
|
client:close()
|
|
end
|
|
return oks, fails, last_status
|
|
end
|
|
|
|
|
|
local add_certificate
|
|
local add_upstream
|
|
local remove_upstream
|
|
local patch_upstream
|
|
local get_upstream
|
|
local get_upstream_health
|
|
local get_balancer_health
|
|
local put_target_address_health
|
|
local get_router_version
|
|
local add_target
|
|
local update_target
|
|
local add_api
|
|
local patch_api
|
|
local gen_multi_host
|
|
local invalidate_router
|
|
do
|
|
local gen_sym
|
|
do
|
|
local sym = 0
|
|
gen_sym = function(name)
|
|
sym = sym + 1
|
|
return name .. "_" .. sym
|
|
end
|
|
end
|
|
|
|
local function api_send(method, path, body, forced_port)
|
|
local api_client = helpers.admin_client(nil, forced_port)
|
|
local res, err = api_client:send({
|
|
method = method,
|
|
path = prefix .. path,
|
|
headers = {
|
|
["Content-Type"] = "application/json"
|
|
},
|
|
body = body,
|
|
})
|
|
if not res then
|
|
api_client:close()
|
|
return nil, err
|
|
end
|
|
local res_body = res.status ~= 204 and cjson.decode((res:read_body()))
|
|
api_client:close()
|
|
return res.status, res_body
|
|
end
|
|
|
|
add_certificate = function(bp, data)
|
|
local certificate_id = uuid()
|
|
local req = kong_table.cycle_aware_deep_copy(data) or {}
|
|
req.id = certificate_id
|
|
bp.certificates:insert(req)
|
|
return certificate_id
|
|
end
|
|
|
|
add_upstream = function(bp, data)
|
|
local upstream_id = uuid()
|
|
local req = kong_table.cycle_aware_deep_copy(data) or {}
|
|
local upstream_name = req.name or gen_sym("upstream")
|
|
req.name = upstream_name
|
|
req.slots = req.slots or SLOTS
|
|
req.id = upstream_id
|
|
bp.upstreams:insert(req)
|
|
return upstream_name, upstream_id
|
|
end
|
|
|
|
remove_upstream = function(bp, upstream_id)
|
|
bp.upstreams:remove({ id = upstream_id })
|
|
end
|
|
|
|
patch_upstream = function(upstream_id, data)
|
|
local res = api_send("PATCH", "/upstreams/" .. upstream_id, data)
|
|
assert(res == 200)
|
|
end
|
|
|
|
get_upstream = function(upstream_id, forced_port)
|
|
local path = "/upstreams/" .. upstream_id
|
|
local status, body = api_send("GET", path, nil, forced_port)
|
|
if status == 200 then
|
|
return body
|
|
end
|
|
end
|
|
|
|
get_upstream_health = function(upstream_id, forced_port)
|
|
local path = "/upstreams/" .. upstream_id .."/health"
|
|
local status, body = api_send("GET", path, nil, forced_port)
|
|
if status == 200 then
|
|
return body
|
|
end
|
|
end
|
|
|
|
get_balancer_health = function(upstream_id, forced_port)
|
|
local path = "/upstreams/" .. upstream_id .."/health?balancer_health=1"
|
|
local status, body = api_send("GET", path, nil, forced_port)
|
|
if status == 200 then
|
|
return body
|
|
end
|
|
end
|
|
|
|
put_target_address_health = function(upstream_id, target_id, address, mode, forced_port)
|
|
local path = "/upstreams/" .. upstream_id .. "/targets/" .. target_id .. "/" .. address .. "/" .. mode
|
|
return api_send("PUT", path, {}, forced_port)
|
|
end
|
|
|
|
get_router_version = function(forced_port)
|
|
local path = "/cache/router:version"
|
|
local status, body = api_send("GET", path, nil, forced_port)
|
|
if status == 200 then
|
|
return body.message
|
|
end
|
|
end
|
|
|
|
invalidate_router = function(forced_port)
|
|
local path = "/cache/router:version"
|
|
local status, body = api_send("DELETE", path, nil, forced_port)
|
|
if status == 204 then
|
|
return true
|
|
end
|
|
|
|
return nil, body
|
|
end
|
|
|
|
do
|
|
local host_num = 0
|
|
gen_multi_host = function()
|
|
host_num = host_num + 1
|
|
return "multiple-hosts-" .. tostring(host_num) .. ".test"
|
|
end
|
|
end
|
|
|
|
add_target = function(bp, upstream_id, host, port, data)
|
|
port = port or get_available_port()
|
|
local req = kong_table.cycle_aware_deep_copy(data) or {}
|
|
if host == "[::1]" then
|
|
host = "[0000:0000:0000:0000:0000:0000:0000:0001]"
|
|
end
|
|
req.target = req.target or format_host(host, port)
|
|
req.weight = req.weight or 10
|
|
req.upstream = { id = upstream_id }
|
|
local new_target = bp.targets:insert(req)
|
|
return port, new_target
|
|
end
|
|
|
|
update_target = function(bp, upstream_id, host, port, data)
|
|
local req = kong_table.cycle_aware_deep_copy(data) or {}
|
|
if host == "[::1]" then
|
|
host = "[0000:0000:0000:0000:0000:0000:0000:0001]"
|
|
end
|
|
req.target = req.target or format_host(host, port)
|
|
req.weight = req.weight or 10
|
|
req.upstream = { id = upstream_id }
|
|
bp.targets:update(req.id or req.target, req)
|
|
end
|
|
|
|
add_api = function(bp, upstream_name, opts)
|
|
opts = opts or {}
|
|
local route_id = uuid()
|
|
local service_id = uuid()
|
|
local route_host = gen_sym("host")
|
|
local sproto = opts.service_protocol or opts.route_protocol or "http"
|
|
local rproto = opts.route_protocol or "http"
|
|
local sport = rproto == "tcp" and 9100 or 80
|
|
|
|
local rpaths = {
|
|
"/",
|
|
"~/(?<namespace>[^/]+)/(?<id>[0-9]+)/?", -- uri capture hash value
|
|
}
|
|
|
|
-- add a 200 route to sync kong async thread
|
|
local route = bp.routes:insert {
|
|
hosts = { "200.test" },
|
|
}
|
|
|
|
bp.plugins:insert {
|
|
route = route,
|
|
name = "request-termination",
|
|
config = {
|
|
status_code = 200,
|
|
message = "Terminated"
|
|
},
|
|
}
|
|
|
|
bp.services:insert({
|
|
id = service_id,
|
|
host = upstream_name,
|
|
port = sport,
|
|
protocol = sproto,
|
|
read_timeout = opts.read_timeout,
|
|
write_timeout = opts.write_timeout,
|
|
connect_timeout = opts.connect_timeout,
|
|
retries = opts.retries,
|
|
})
|
|
bp.routes:insert({
|
|
id = route_id,
|
|
service = { id = service_id },
|
|
protocols = { rproto },
|
|
hosts = rproto ~= "tcp" and { route_host } or nil,
|
|
destinations = (rproto == "tcp") and {{ port = 9100 }} or nil,
|
|
paths = rproto ~= "tcp" and rpaths or nil,
|
|
})
|
|
|
|
bp.plugins:insert({
|
|
name = "post-function",
|
|
service = { id = service_id },
|
|
config = {
|
|
header_filter = {[[
|
|
local value = ngx.ctx and
|
|
ngx.ctx.balancer_data and
|
|
ngx.ctx.balancer_data.hash_value
|
|
if value == "" or value == nil then
|
|
value = "NONE"
|
|
end
|
|
|
|
ngx.header["x-balancer-hash-value"] = value
|
|
ngx.header["x-uri"] = ngx.var.request_uri
|
|
]]},
|
|
},
|
|
})
|
|
|
|
return route_host, service_id, route_id
|
|
end
|
|
|
|
patch_api = function(bp, service_id, new_upstream, read_timeout)
|
|
bp.services:update(service_id, {
|
|
url = new_upstream,
|
|
read_timeout = read_timeout,
|
|
})
|
|
end
|
|
end
|
|
|
|
|
|
local poll_wait_health
|
|
local poll_wait_address_health
|
|
do
|
|
local function poll_wait(upstream_id, host, port, admin_port, fn)
|
|
if host == "[::1]" then
|
|
host = "[0000:0000:0000:0000:0000:0000:0000:0001]"
|
|
end
|
|
local hard_timeout = ngx.now() + 70
|
|
while ngx.now() < hard_timeout do
|
|
local health = get_upstream_health(upstream_id, admin_port)
|
|
if health then
|
|
for _, d in ipairs(health.data) do
|
|
if d.target == host .. ":" .. port and fn(d) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
ngx.sleep(0.1) -- poll-wait
|
|
end
|
|
return false
|
|
end
|
|
|
|
poll_wait_health = function(upstream_id, host, port, value, admin_port)
|
|
local ok = poll_wait(upstream_id, host, port, admin_port, function(d)
|
|
return d.health == value
|
|
end)
|
|
if ok then
|
|
return true
|
|
end
|
|
assert(false, "timed out waiting for " .. host .. ":" .. port .. " in " ..
|
|
upstream_id .. " to become " .. value)
|
|
end
|
|
|
|
poll_wait_address_health = function(upstream_id, host, port, address_host, address_port, value)
|
|
local ok = poll_wait(upstream_id, host, port, nil, function(d)
|
|
for _, ad in ipairs(d.data.addresses) do
|
|
if ad.ip == address_host
|
|
and ad.port == address_port
|
|
and ad.health == value then
|
|
return true
|
|
end
|
|
end
|
|
end)
|
|
if ok then
|
|
return true
|
|
end
|
|
assert(false, "timed out waiting for " .. address_host .. ":" .. address_port .. " in " ..
|
|
upstream_id .. " to become " .. value)
|
|
end
|
|
end
|
|
|
|
|
|
local function wait_for_router_update(bp, old_rv, localhost, proxy_port, admin_port)
|
|
-- add dummy upstream just to rebuild router
|
|
local dummy_upstream_name, dummy_upstream_id = add_upstream(bp)
|
|
local dummy_port = add_target(bp, dummy_upstream_id, localhost)
|
|
local dummy_api_host = add_api(bp, dummy_upstream_name)
|
|
local dummy_server = https_server.new(dummy_port, localhost)
|
|
dummy_server:start()
|
|
|
|
-- forces the router to be rebuild, reduces the flakiness of the test suite
|
|
-- TODO: find out what's wrong with router invalidation in the particular
|
|
-- test setup causing the flakiness
|
|
assert(invalidate_router(admin_port))
|
|
|
|
helpers.wait_until(function()
|
|
client_requests(1, dummy_api_host, "127.0.0.1", proxy_port)
|
|
local rv = get_router_version(admin_port)
|
|
return rv ~= old_rv
|
|
end, 5)
|
|
|
|
dummy_server:shutdown()
|
|
end
|
|
|
|
|
|
local function tcp_client_requests(nreqs, host, port)
|
|
local fails, ok1, ok2 = 0, 0, 0
|
|
for _ = 1, nreqs do
|
|
local sock = ngx.socket.tcp()
|
|
assert(sock:connect(host, port))
|
|
assert(sock:send("hello\n"))
|
|
local response, err = sock:receive()
|
|
if err then
|
|
fails = fails + 1
|
|
elseif response:match("^1 ") then
|
|
ok1 = ok1 + 1
|
|
elseif response:match("^2 ") then
|
|
ok2 = ok2 + 1
|
|
end
|
|
end
|
|
return ok1, ok2, fails
|
|
end
|
|
|
|
|
|
local function begin_testcase_setup(strategy, bp)
|
|
if strategy == "off" then
|
|
bp.done()
|
|
end
|
|
end
|
|
|
|
local function begin_testcase_setup_update(strategy, bp)
|
|
if strategy == "off" then
|
|
bp.reset_back()
|
|
end
|
|
end
|
|
|
|
|
|
local function end_testcase_setup(strategy, bp)
|
|
if strategy == "off" then
|
|
-- setup some dummy entities for checking the config update status
|
|
local host = "localhost"
|
|
local port = get_available_port()
|
|
|
|
local server = https_server.new(port, host, "http", nil, 1)
|
|
server:start()
|
|
|
|
local upstream_name, upstream_id = add_upstream(bp)
|
|
add_target(bp, upstream_id, host, port)
|
|
local api_host = add_api(bp, upstream_name)
|
|
|
|
local cfg = bp.done()
|
|
local yaml = declarative.to_yaml_string(cfg)
|
|
local admin_client = helpers.admin_client()
|
|
local res = assert(admin_client:send {
|
|
method = "POST",
|
|
path = "/config",
|
|
body = {
|
|
config = yaml,
|
|
},
|
|
headers = {
|
|
["Content-Type"] = "multipart/form-data",
|
|
}
|
|
})
|
|
assert(res ~= nil)
|
|
assert(res.status == 201)
|
|
admin_client:close()
|
|
|
|
local ok, err = pcall(function ()
|
|
-- wait for dummy config ready
|
|
helpers.pwait_until(function ()
|
|
local oks = client_requests(3, api_host)
|
|
assert(oks == 3)
|
|
end, 15)
|
|
end)
|
|
|
|
server:shutdown()
|
|
|
|
|
|
if not ok then
|
|
error(err)
|
|
end
|
|
|
|
else
|
|
helpers.wait_for_all_config_update()
|
|
end
|
|
end
|
|
|
|
|
|
local function get_db_utils_for_dc_and_admin_api(strategy, tables)
|
|
local bp = assert(helpers.get_db_utils(strategy, tables))
|
|
if strategy ~= "off" then
|
|
bp = require("spec.fixtures.admin_api")
|
|
end
|
|
return bp
|
|
end
|
|
|
|
|
|
local function setup_prefix(p)
|
|
prefix = p
|
|
local bp = require("spec.fixtures.admin_api")
|
|
bp.set_prefix(prefix)
|
|
end
|
|
|
|
|
|
local function teardown_prefix()
|
|
prefix = ""
|
|
local bp = require("spec.fixtures.admin_api")
|
|
bp.set_prefix(prefix)
|
|
end
|
|
|
|
|
|
local function test_with_prefixes(itt, strategy, prefixes)
|
|
return function(description, fn)
|
|
if strategy == "off" then
|
|
itt(description, fn)
|
|
return
|
|
end
|
|
|
|
for _, name in ipairs(prefixes) do
|
|
itt(name .. ": " .. description, function()
|
|
setup_prefix("/" .. name)
|
|
local ok = fn()
|
|
teardown_prefix()
|
|
return ok
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local localhosts = {
|
|
ipv4 = "127.0.0.1",
|
|
ipv6 = "[::1]",
|
|
hostname = "localhost",
|
|
}
|
|
|
|
|
|
local consistencies = {"strict", "eventual"}
|
|
|
|
|
|
local balancer_utils = {}
|
|
--balancer_utils.
|
|
balancer_utils.add_certificate = add_certificate
|
|
balancer_utils.add_api = add_api
|
|
balancer_utils.add_target = add_target
|
|
balancer_utils.update_target = update_target
|
|
balancer_utils.add_upstream = add_upstream
|
|
balancer_utils.remove_upstream = remove_upstream
|
|
balancer_utils.begin_testcase_setup = begin_testcase_setup
|
|
balancer_utils.begin_testcase_setup_update = begin_testcase_setup_update
|
|
balancer_utils.client_requests = client_requests
|
|
balancer_utils.consistencies = consistencies
|
|
balancer_utils.CONSISTENCY_FREQ = CONSISTENCY_FREQ
|
|
balancer_utils.direct_request = direct_request
|
|
balancer_utils.end_testcase_setup = end_testcase_setup
|
|
balancer_utils.gen_multi_host = gen_multi_host
|
|
balancer_utils.get_available_port = get_available_port
|
|
balancer_utils.get_balancer_health = get_balancer_health
|
|
balancer_utils.get_db_utils_for_dc_and_admin_api = get_db_utils_for_dc_and_admin_api
|
|
balancer_utils.get_router_version = get_router_version
|
|
balancer_utils.get_upstream = get_upstream
|
|
balancer_utils.get_upstream_health = get_upstream_health
|
|
balancer_utils.healthchecks_config = healthchecks_config
|
|
balancer_utils.HEALTHCHECK_INTERVAL = HEALTHCHECK_INTERVAL
|
|
balancer_utils.localhosts = localhosts
|
|
balancer_utils.patch_api = patch_api
|
|
balancer_utils.patch_upstream = patch_upstream
|
|
balancer_utils.poll_wait_address_health = poll_wait_address_health
|
|
balancer_utils.poll_wait_health = poll_wait_health
|
|
balancer_utils.put_target_address_health = put_target_address_health
|
|
balancer_utils.put_target_endpoint = put_target_endpoint
|
|
balancer_utils.SLOTS = SLOTS
|
|
balancer_utils.tcp_client_requests = tcp_client_requests
|
|
balancer_utils.wait_for_router_update = wait_for_router_update
|
|
balancer_utils.test_with_prefixes = test_with_prefixes
|
|
|
|
|
|
return balancer_utils
|