From 00d1c3ff8afddb1b9c2600701d00c7655ef9f828 Mon Sep 17 00:00:00 2001 From: Chrono Date: Wed, 17 Apr 2024 22:39:26 +0800 Subject: [PATCH] feat(db/schema): add `traditional_compatible` fields to `expressions` flavor (#12667) Previously, setting `router_flavor = expressions` makes traditional fields disappear. This commit makes these field available alongside the `expression` field, and user can configure expressions route alongside traditional compatible but not at the same time inside the same route entry. When requests comes in, expressions route are evaluated first, and if none matches, traditional routes will be evaluated in the usual order. KAG-3805 KAG-3807 --------- Co-authored-by: Datong Sun --- ...xpressions-supports-traditional-fields.yml | 7 + kong/db/schema/entities/routes.lua | 220 ++++++++---------- kong/db/schema/entities/routes_subschemas.lua | 55 +++-- kong/router/atc.lua | 6 + kong/router/compat.lua | 95 -------- kong/router/expressions.lua | 12 +- kong/router/transform.lua | 113 ++++++++- .../01-db/01-schema/06-routes_spec.lua | 108 +++++++-- spec/01-unit/08-router_spec.lua | 109 +++++++-- .../02-integration/05-proxy/01-proxy_spec.lua | 11 +- .../05-proxy/02-router_spec.lua | 73 +++++- spec/02-integration/05-proxy/06-ssl_spec.lua | 13 +- .../05-proxy/10-balancer/06-stream_spec.lua | 11 +- .../05-proxy/18-upstream_tls_spec.lua | 17 +- .../05-proxy/19-grpc_proxy_spec.lua | 13 +- .../21-grpc_plugins_triggering_spec.lua | 13 +- .../05-proxy/23-context_spec.lua | 12 +- spec/02-integration/05-proxy/26-udp_spec.lua | 11 +- .../28-stream_plugins_triggering_spec.lua | 11 +- .../05-proxy/31-stream_tls_spec.lua | 2 +- 20 files changed, 545 insertions(+), 367 deletions(-) create mode 100644 changelog/unreleased/kong/flavor-expressions-supports-traditional-fields.yml diff --git a/changelog/unreleased/kong/flavor-expressions-supports-traditional-fields.yml b/changelog/unreleased/kong/flavor-expressions-supports-traditional-fields.yml new file mode 100644 index 000000000000..e6169297461e --- /dev/null +++ b/changelog/unreleased/kong/flavor-expressions-supports-traditional-fields.yml @@ -0,0 +1,7 @@ +message: | + Supported fields `methods`, `hosts`, `paths`, `headers`, + `snis`, `sources`, `destinations` and `regex_priority` + for the `route` entity when the `router_flavor` is `expressions`. + The meaning of these fields are consistent with the traditional route entity. +type: feature +scope: Core diff --git a/kong/db/schema/entities/routes.lua b/kong/db/schema/entities/routes.lua index d166c70d29fa..806b263cb240 100644 --- a/kong/db/schema/entities/routes.lua +++ b/kong/db/schema/entities/routes.lua @@ -1,25 +1,84 @@ local typedefs = require("kong.db.schema.typedefs") local deprecation = require("kong.deprecation") + local kong_router_flavor = kong and kong.configuration and kong.configuration.router_flavor + +local PATH_V1_DEPRECATION_MSG = + "path_handling='v1' is deprecated and " .. + (kong_router_flavor == "traditional" and + "will be removed in future version, " or + "will not work under 'expressions' or 'traditional_compatible' router_flavor, ") .. + "please use path_handling='v0' instead" + + +local entity_checks = { + { conditional = { if_field = "protocols", + if_match = { elements = { type = "string", not_one_of = { "grpcs", "https", "tls", "tls_passthrough" }}}, + then_field = "snis", + then_match = { len_eq = 0 }, + then_err = "'snis' can only be set when 'protocols' is 'grpcs', 'https', 'tls' or 'tls_passthrough'", + } + }, + + { custom_entity_check = { + field_sources = { "path_handling" }, + fn = function(entity) + if entity.path_handling == "v1" then + deprecation(PATH_V1_DEPRECATION_MSG, { after = "3.0", }) + end + + return true + end, + }}, +} + + -- works with both `traditional_compatible` and `expressions` routes local validate_route -if kong_router_flavor ~= "traditional" then +if kong_router_flavor == "traditional_compatible" or kong_router_flavor == "expressions" then local ipairs = ipairs local tonumber = tonumber local re_match = ngx.re.match local router = require("resty.router.router") + local transform = require("kong.router.transform") local get_schema = require("kong.router.atc").schema local get_expression = kong_router_flavor == "traditional_compatible" and require("kong.router.compat").get_expression or require("kong.router.expressions").transform_expression + local is_null = transform.is_null + local is_empty_field = transform.is_empty_field + local HTTP_PATH_SEGMENTS_PREFIX = "http.path.segments." local HTTP_PATH_SEGMENTS_SUFFIX_REG = [[^(0|[1-9]\d*)(_([1-9]\d*))?$]] validate_route = function(entity) + local is_expression_empty = + is_null(entity.expression) -- expression is not a table + + local is_others_empty = + is_empty_field(entity.snis) and + is_empty_field(entity.sources) and + is_empty_field(entity.destinations) and + is_empty_field(entity.methods) and + is_empty_field(entity.hosts) and + is_empty_field(entity.paths) and + is_empty_field(entity.headers) + + if is_expression_empty and is_others_empty then + return true + end + + if not is_expression_empty and not is_others_empty then + return nil, "Router Expression failed validation: " .. + "cannot set 'expression' with " .. + "'methods', 'hosts', 'paths', 'headers', 'snis', 'sources' or 'destinations' " .. + "simultaneously" + end + local schema = get_schema(entity.protocols) local exp = get_expression(entity) @@ -43,20 +102,34 @@ if kong_router_flavor ~= "traditional" then return true end + + table.insert(entity_checks, + { custom_entity_check = { + field_sources = { "id", "protocols", + "snis", "sources", "destinations", + "methods", "hosts", "paths", "headers", + "expression", + }, + run_with_missing_fields = true, + fn = validate_route, + } } + ) end -- if kong_router_flavor ~= "traditional" -if kong_router_flavor == "expressions" then - return { + +local routes = { name = "routes", primary_key = { "id" }, endpoint_key = "name", workspaceable = true, + subschema_key = "protocols", fields = { { id = typedefs.uuid, }, { created_at = typedefs.auto_timestamp_s }, { updated_at = typedefs.auto_timestamp_s }, { name = typedefs.utf8_name }, + { protocols = { type = "set", description = "An array of the protocols this Route should allow.", len_min = 1, @@ -70,6 +143,7 @@ if kong_router_flavor == "expressions" then }, default = { "http", "https" }, -- TODO: different default depending on service's scheme }, }, + { https_redirect_status_code = { type = "integer", description = "The status code Kong responds with when all properties of a Route match except the protocol", one_of = { 426, 301, 302, 307, 308 }, @@ -79,109 +153,16 @@ if kong_router_flavor == "expressions" then { preserve_host = { description = "When matching a Route via one of the hosts domain names, use the request Host header in the upstream request headers.", type = "boolean", required = true, default = false }, }, { request_buffering = { description = "Whether to enable request body buffering or not. With HTTP 1.1.", type = "boolean", required = true, default = true }, }, { response_buffering = { description = "Whether to enable response body buffering or not.", type = "boolean", required = true, default = true }, }, + { tags = typedefs.tags }, { service = { description = "The Service this Route is associated to. This is where the Route proxies traffic to.", type = "foreign", reference = "services" }, }, - { expression = { description = " The router expression.", type = "string", required = true }, }, - { priority = { description = "A number used to choose which route resolves a given request when several routes match it using regexes simultaneously.", type = "integer", required = true, default = 0 }, }, - }, - - entity_checks = { - { custom_entity_check = { - field_sources = { "expression", "id", "protocols", }, - fn = validate_route, - } }, - }, - } - --- router_flavor in ('traditional_compatible', 'traditional') -else - local PATH_V1_DEPRECATION_MSG - - if kong_router_flavor == "traditional" then - PATH_V1_DEPRECATION_MSG = - "path_handling='v1' is deprecated and " .. - "will be removed in future version, " .. - "please use path_handling='v0' instead" - - elseif kong_router_flavor == "traditional_compatible" then - PATH_V1_DEPRECATION_MSG = - "path_handling='v1' is deprecated and " .. - "will not work under 'traditional_compatible' router_flavor, " .. - "please use path_handling='v0' instead" - end - - local entity_checks = { - { conditional = { if_field = "protocols", - if_match = { elements = { type = "string", not_one_of = { "grpcs", "https", "tls", "tls_passthrough" }}}, - then_field = "snis", - then_match = { len_eq = 0 }, - then_err = "'snis' can only be set when 'protocols' is 'grpcs', 'https', 'tls' or 'tls_passthrough'", - }}, - { custom_entity_check = { - field_sources = { "path_handling" }, - fn = function(entity) - if entity.path_handling == "v1" then - deprecation(PATH_V1_DEPRECATION_MSG, { after = "3.0", }) - end - - return true - end, - }}, - } - - if kong_router_flavor == "traditional_compatible" then - local is_empty_field = require("kong.router.transform").is_empty_field - - table.insert(entity_checks, - { custom_entity_check = { - field_sources = { "id", "protocols", - "snis", "sources", "destinations", - "methods", "hosts", "paths", "headers", - }, - run_with_missing_fields = true, - fn = function(entity) - if is_empty_field(entity.snis) and - is_empty_field(entity.sources) and - is_empty_field(entity.destinations) and - is_empty_field(entity.methods) and - is_empty_field(entity.hosts) and - is_empty_field(entity.paths) and - is_empty_field(entity.headers) - then - return true - end - - return validate_route(entity) - end, - }} - ) - end - return { - name = "routes", - primary_key = { "id" }, - endpoint_key = "name", - workspaceable = true, - subschema_key = "protocols", + { snis = { type = "set", + description = "A list of SNIs that match this Route.", + elements = typedefs.sni }, }, + { sources = typedefs.sources }, + { destinations = typedefs.destinations }, - fields = { - { id = typedefs.uuid, }, - { created_at = typedefs.auto_timestamp_s }, - { updated_at = typedefs.auto_timestamp_s }, - { name = typedefs.utf8_name }, - { protocols = { type = "set", - description = "An array of the protocols this Route should allow.", - len_min = 1, - required = true, - elements = typedefs.protocol, - mutually_exclusive_subsets = { - { "http", "https" }, - { "tcp", "tls", "udp" }, - { "tls_passthrough" }, - { "grpc", "grpcs" }, - }, - default = { "http", "https" }, -- TODO: different default depending on service's scheme - }, }, { methods = typedefs.methods }, { hosts = typedefs.hosts }, { paths = typedefs.router_paths }, @@ -195,27 +176,26 @@ else }, }, } }, - { https_redirect_status_code = { type = "integer", - description = "The status code Kong responds with when all properties of a Route match except the protocol", - one_of = { 426, 301, 302, 307, 308 }, - default = 426, required = true, - }, }, + { regex_priority = { description = "A number used to choose which route resolves a given request when several routes match it using regexes simultaneously.", type = "integer", default = 0 }, }, - { strip_path = { description = "When matching a Route via one of the paths, strip the matching prefix from the upstream request URL.", type = "boolean", required = true, default = true }, }, { path_handling = { description = "Controls how the Service path, Route path and requested path are combined when sending a request to the upstream.", type = "string", default = "v0", one_of = { "v0", "v1" }, }, }, - { preserve_host = { description = "When matching a Route via one of the hosts domain names, use the request Host header in the upstream request headers.", type = "boolean", required = true, default = false }, }, - { request_buffering = { description = "Whether to enable request body buffering or not. With HTTP 1.1.", type = "boolean", required = true, default = true }, }, - { response_buffering = { description = "Whether to enable response body buffering or not.", type = "boolean", required = true, default = true }, }, - { snis = { type = "set", - description = "A list of SNIs that match this Route when using stream routing.", - elements = typedefs.sni }, }, - { sources = typedefs.sources }, - { destinations = typedefs.destinations }, - { tags = typedefs.tags }, - { service = { description = "The Service this Route is associated to. This is where the Route proxies traffic to.", - type = "foreign", reference = "services" }, }, - }, + }, -- fields entity_checks = entity_checks, +} -- routes + + +if kong_router_flavor == "expressions" then + + local special_fields = { + { expression = { description = "The route expression.", type = "string" }, }, -- not required now + { priority = { description = "A number used to specify the matching order for expression routes. The higher the `priority`, the sooner an route will be evaluated. This field is ignored unless `expression` field is set.", type = "integer", between = { 0, 2^46 - 1 }, required = true, default = 0 }, }, } + + for _, v in ipairs(special_fields) do + table.insert(routes.fields, v) + end end + + +return routes diff --git a/kong/db/schema/entities/routes_subschemas.lua b/kong/db/schema/entities/routes_subschemas.lua index 0801c5bb99f3..7ac3dff19330 100644 --- a/kong/db/schema/entities/routes_subschemas.lua +++ b/kong/db/schema/entities/routes_subschemas.lua @@ -66,18 +66,47 @@ local grpc_subschema = { } +-- NOTICE: make sure we have correct schema constraion for flavor 'expressions' if kong and kong.configuration and kong.configuration.router_flavor == "expressions" then - return {} - -else - return { - http = http_subschema, -- protocols is the subschema key, and the first - https = http_subschema, -- matching protocol name is selected as subschema name - tcp = stream_subschema, - tls = stream_subschema, - udp = stream_subschema, - tls_passthrough = stream_subschema, - grpc = grpc_subschema, - grpcs = grpc_subschema, - } + + -- now http route in flavor 'expressions' accepts `sources` and `destinations` + + assert(http_subschema.fields[1].sources) + http_subschema.fields[1] = nil -- sources + + assert(http_subschema.fields[2].destinations) + http_subschema.fields[2] = nil -- destinations + + -- the route should have the field 'expression' if no others + + table.insert(http_subschema.entity_checks[1].conditional_at_least_one_of.then_at_least_one_of, "expression") + table.insert(http_subschema.entity_checks[1].conditional_at_least_one_of.else_then_at_least_one_of, "expression") + + -- now grpc route in flavor 'expressions' accepts `sources` and `destinations` + + assert(grpc_subschema.fields[3].sources) + grpc_subschema.fields[3] = nil -- sources + + assert(grpc_subschema.fields[4].destinations) + grpc_subschema.fields[4] = nil -- destinations + + -- the route should have the field 'expression' if no others + + table.insert(grpc_subschema.entity_checks[1].conditional_at_least_one_of.then_at_least_one_of, "expression") + table.insert(grpc_subschema.entity_checks[1].conditional_at_least_one_of.else_then_at_least_one_of, "expression") + + table.insert(stream_subschema.entity_checks[1].conditional_at_least_one_of.then_at_least_one_of, "expression") + table.insert(stream_subschema.entity_checks[2].conditional_at_least_one_of.then_at_least_one_of, "expression") + end + +return { + http = http_subschema, -- protocols is the subschema key, and the first + https = http_subschema, -- matching protocol name is selected as subschema name + tcp = stream_subschema, + tls = stream_subschema, + udp = stream_subschema, + tls_passthrough = stream_subschema, + grpc = grpc_subschema, + grpcs = grpc_subschema, +} diff --git a/kong/router/atc.lua b/kong/router/atc.lua index f7043491d1a1..55dd8d2db755 100644 --- a/kong/router/atc.lua +++ b/kong/router/atc.lua @@ -30,6 +30,7 @@ local check_select_params = utils.check_select_params local get_service_info = utils.get_service_info local route_match_stat = utils.route_match_stat local split_host_port = transform.split_host_port +local split_routes_and_services_by_path = transform.split_routes_and_services_by_path local DEFAULT_MATCH_LRUCACHE_SIZE = utils.DEFAULT_MATCH_LRUCACHE_SIZE @@ -277,10 +278,15 @@ end function _M.new(routes, cache, cache_neg, old_router, get_exp_and_priority) + -- routes argument is a table with [route] and [service] if type(routes) ~= "table" then return error("expected arg #1 routes to be a table") end + if is_http then + routes = split_routes_and_services_by_path(routes) + end + local router, err if not old_router then diff --git a/kong/router/compat.lua b/kong/router/compat.lua index 410168e85753..a3f3f21e1d00 100644 --- a/kong/router/compat.lua +++ b/kong/router/compat.lua @@ -2,38 +2,13 @@ local _M = {} local atc = require("kong.router.atc") -local utils = require("kong.router.utils") local transform = require("kong.router.transform") -local tb_new = require("table.new") -local tb_nkeys = require("table.nkeys") -local uuid = require("resty.jit-uuid") -local shallow_copy = require("kong.tools.utils").shallow_copy - - -local is_regex_magic = utils.is_regex_magic -local is_empty_field = transform.is_empty_field local get_expression = transform.get_expression local get_priority = transform.get_priority -local type = type -local pairs = pairs -local ipairs = ipairs -local assert = assert -local tb_insert = table.insert - - -local is_http = ngx.config.subsystem == "http" - - --- When splitting routes, we need to assign new UUIDs to the split routes. We use uuid v5 to generate them from --- the original route id and the path index so that incremental rebuilds see stable IDs for routes that have not --- changed. -local uuid_generator = assert(uuid.factory_v5('7f145bf9-0dce-4f91-98eb-debbce4b9f6b')) - - local function get_exp_and_priority(route) if route.expression then ngx.log(ngx.ERR, "expecting a traditional route while it's not (probably an expressions route). ", @@ -47,77 +22,7 @@ local function get_exp_and_priority(route) end --- group array-like table t by the function f, returning a table mapping from --- the result of invoking f on one of the elements to the actual elements. -local function group_by(t, f) - local result = {} - for _, value in ipairs(t) do - local key = f(value) - if result[key] then - tb_insert(result[key], value) - else - result[key] = { value } - end - end - return result -end - --- split routes into multiple routes, one for each prefix length and one for all --- regular expressions -local function split_route_by_path_into(route_and_service, routes_and_services_split) - local original_route = route_and_service.route - - if is_empty_field(original_route.paths) or #original_route.paths == 1 then - tb_insert(routes_and_services_split, route_and_service) - return - end - - -- make sure that route_and_service contains only the two expected entries, route and service - assert(tb_nkeys(route_and_service) == 1 or tb_nkeys(route_and_service) == 2) - - local grouped_paths = group_by( - original_route.paths, - function(path) - return is_regex_magic(path) or #path - end - ) - for index, paths in pairs(grouped_paths) do - local cloned_route = { - route = shallow_copy(original_route), - service = route_and_service.service, - } - - cloned_route.route.original_route = original_route - cloned_route.route.paths = paths - cloned_route.route.id = uuid_generator(original_route.id .. "#" .. tostring(index)) - - tb_insert(routes_and_services_split, cloned_route) - end -end - - -local function split_routes_and_services_by_path(routes_and_services) - local count = #routes_and_services - local routes_and_services_split = tb_new(count, 0) - - for i = 1, count do - split_route_by_path_into(routes_and_services[i], routes_and_services_split) - end - - return routes_and_services_split -end - - function _M.new(routes_and_services, cache, cache_neg, old_router) - -- route_and_service argument is a table with [route] and [service] - if type(routes_and_services) ~= "table" then - return error("expected arg #1 routes to be a table", 2) - end - - if is_http then - routes_and_services = split_routes_and_services_by_path(routes_and_services) - end - return atc.new(routes_and_services, cache, cache_neg, old_router, get_exp_and_priority) end diff --git a/kong/router/expressions.lua b/kong/router/expressions.lua index 733aaeb88c66..7d11022344e9 100644 --- a/kong/router/expressions.lua +++ b/kong/router/expressions.lua @@ -8,6 +8,10 @@ local atc = require("kong.router.atc") local transform = require("kong.router.transform") +local get_expression = transform.get_expression +local get_priority = transform.get_priority + + local gen_for_field = transform.gen_for_field local OP_EQUAL = transform.OP_EQUAL local LOGICAL_AND = transform.LOGICAL_AND @@ -27,7 +31,7 @@ local PROTOCOLS_OVERRIDE = { -- net.port => net.dst.port local function transform_expression(route) - local exp = route.expression + local exp = get_expression(route) if not exp then return nil @@ -60,6 +64,8 @@ local function get_exp_and_priority(route) return end + local priority = get_priority(route) + local protocols = route.protocols -- give the chance for http redirection (301/302/307/308/426) @@ -69,7 +75,7 @@ local function get_exp_and_priority(route) protocols[1] == "tls" or protocols[1] == "tls_passthrough") then - return exp, route.priority + return exp, priority end local gen = gen_for_field("net.protocol", OP_EQUAL, protocols, @@ -80,7 +86,7 @@ local function get_exp_and_priority(route) exp = exp .. LOGICAL_AND .. gen end - return exp, route.priority + return exp, priority end diff --git a/kong/router/transform.lua b/kong/router/transform.lua index 02c74676851c..351eb480e68b 100644 --- a/kong/router/transform.lua +++ b/kong/router/transform.lua @@ -1,6 +1,8 @@ local bit = require("bit") local buffer = require("string.buffer") +local tb_new = require("table.new") local tb_nkeys = require("table.nkeys") +local uuid = require("resty.jit-uuid") local lrucache = require("resty.lrucache") local ipmatcher = require("resty.ipmatcher") local utils = require("kong.router.utils") @@ -8,7 +10,10 @@ local utils = require("kong.router.utils") local type = type local assert = assert +local pairs = pairs local ipairs = ipairs +local tostring = tostring +local tb_insert = table.insert local fmt = string.format local byte = string.byte local bor, band, lshift, rshift = bit.bor, bit.band, bit.lshift, bit.rshift @@ -16,6 +21,7 @@ local bor, band, lshift, rshift = bit.bor, bit.band, bit.lshift, bit.rshift local is_regex_magic = utils.is_regex_magic local replace_dashes_lower = require("kong.tools.string").replace_dashes_lower +local shallow_copy = require("kong.tools.table").shallow_copy local is_null @@ -305,6 +311,13 @@ end local function get_expression(route) + -- we perfer the field 'expression', reject others + if not is_null(route.expression) then + return route.expression + end + + -- transform other fields (methods/hosts/paths/...) to expression + local methods = route.methods local hosts = route.hosts local paths = route.paths @@ -512,6 +525,10 @@ local PLAIN_HOST_ONLY_BIT = lshift_uint64(0x01ULL, 60) local REGEX_URL_BIT = lshift_uint64(0x01ULL, 51) +-- expression only route has higher priority than traditional route +local EXPRESSION_ONLY_BIT = lshift_uint64(0xFFULL, 56) + + -- convert a route to a priority value for use in the ATC router -- priority must be a 64-bit non negative integer -- format (big endian): @@ -527,6 +544,13 @@ local REGEX_URL_BIT = lshift_uint64(0x01ULL, 51) -- | | | -- +-------------------------+-------------------------------------+ local function get_priority(route) + -- we perfer the fields 'expression' and 'priority' + if not is_null(route.expression) then + return bor(EXPRESSION_ONLY_BIT, route.priority or 0) + end + + -- transform other fields (methods/hosts/paths/...) to expression priority + local snis = route.snis local srcs = route.sources local dsts = route.destinations @@ -544,7 +568,15 @@ local function get_priority(route) local paths = route.paths local headers = route.headers - local match_weight = 0 -- 0x0ULL + local match_weight = 0 -- 0x0ULL, *can not* exceed `7` + + if not is_empty_field(srcs) then + match_weight = match_weight + 1 + end + + if not is_empty_field(dsts) then + match_weight = match_weight + 1 + end if not is_empty_field(methods) then match_weight = match_weight + 1 @@ -611,6 +643,10 @@ local function get_priority(route) end end + -- Currently match_weight has only 3 bits + -- it can not be more than 7 + assert(match_weight <= 7) + local match_weight = lshift_uint64(match_weight, 61) local headers_count = lshift_uint64(headers_count, 52) @@ -628,6 +664,77 @@ local function get_priority(route) end +-- When splitting routes, we need to assign new UUIDs to the split routes. We use uuid v5 to generate them from +-- the original route id and the path index so that incremental rebuilds see stable IDs for routes that have not +-- changed. +local uuid_generator = assert(uuid.factory_v5('7f145bf9-0dce-4f91-98eb-debbce4b9f6b')) + + +local function sort_by_regex_or_length(path) + return is_regex_magic(path) or #path +end + + +-- group array-like table t by the function f, returning a table mapping from +-- the result of invoking f on one of the elements to the actual elements. +local function group_by(t, f) + local result = {} + for _, value in ipairs(t) do + local key = f(value) + if result[key] then + tb_insert(result[key], value) + else + result[key] = { value } + end + end + return result +end + +-- split routes into multiple routes, one for each prefix length and one for all +-- regular expressions +local function split_route_by_path_info(route_and_service, routes_and_services_split) + local original_route = route_and_service.route + + if is_empty_field(original_route.paths) or #original_route.paths == 1 or + not is_null(original_route.expression) -- expression will ignore paths + then + tb_insert(routes_and_services_split, route_and_service) + return + end + + -- make sure that route_and_service contains only the two expected entries, route and service + assert(tb_nkeys(route_and_service) == 1 or tb_nkeys(route_and_service) == 2) + + local grouped_paths = group_by( + original_route.paths, sort_by_regex_or_length + ) + for index, paths in pairs(grouped_paths) do + local cloned_route = { + route = shallow_copy(original_route), + service = route_and_service.service, + } + + cloned_route.route.original_route = original_route + cloned_route.route.paths = paths + cloned_route.route.id = uuid_generator(original_route.id .. "#" .. tostring(index)) + + tb_insert(routes_and_services_split, cloned_route) + end +end + + +local function split_routes_and_services_by_path(routes_and_services) + local count = #routes_and_services + local routes_and_services_split = tb_new(count, 0) + + for i = 1, count do + split_route_by_path_info(routes_and_services[i], routes_and_services_split) + end + + return routes_and_services_split +end + + return { OP_EQUAL = OP_EQUAL, @@ -636,10 +743,14 @@ return { split_host_port = split_host_port, + is_null = is_null, is_empty_field = is_empty_field, + gen_for_field = gen_for_field, get_expression = get_expression, get_priority = get_priority, + + split_routes_and_services_by_path = split_routes_and_services_by_path, } diff --git a/spec/01-unit/01-db/01-schema/06-routes_spec.lua b/spec/01-unit/01-db/01-schema/06-routes_spec.lua index e8b788818f89..a3d551cab9c7 100644 --- a/spec/01-unit/01-db/01-schema/06-routes_spec.lua +++ b/spec/01-unit/01-db/01-schema/06-routes_spec.lua @@ -43,14 +43,17 @@ local function reload_flavor(flavor) end -describe("routes schema (flavor = traditional/traditional_compatible)", function() +for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do +describe("routes schema (flavor = " .. flavor .. ")", function() local a_valid_uuid = "cbb297c0-a956-486d-ad1d-f9b42df9465a" local another_uuid = "64a8670b-900f-44e7-a900-6ec7ef5aa4d3" local uuid_pattern = "^" .. ("%x"):rep(8) .. "%-" .. ("%x"):rep(4) .. "%-" .. ("%x"):rep(4) .. "%-" .. ("%x"):rep(4) .. "%-" .. ("%x"):rep(12) .. "$" - reload_flavor("traditional") + local it_trad_only = (flavor == "traditional") and it or pending + + reload_flavor(flavor) setup_global_env() it("validates a valid route", function() @@ -354,7 +357,8 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function assert.is_true(ok) end) - it("accepts properly percent-encoded values", function() + -- TODO: bump atc-router to fix it + it_trad_only("accepts properly percent-encoded values", function() local valid_paths = { "/abcd\xaa\x10\xff\xAA\xFF" } for i = 1, #valid_paths do @@ -1070,7 +1074,8 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function assert.falsy(ok) assert.same({ ["@entity"] = { - "must set one of 'sources', 'destinations', 'snis' when 'protocols' is 'tcp', 'tls' or 'udp'" + "must set one of 'sources', 'destinations', 'snis'" .. + (flavor == "expressions" and ", 'expression'" or "") .. " when 'protocols' is 'tcp', 'tls' or 'udp'" } }, errs) end @@ -1087,7 +1092,8 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function assert.falsy(ok) assert.same({ ["@entity"] = { - "must set one of 'methods', 'hosts', 'headers', 'paths' when 'protocols' is 'http'" + "must set one of 'methods', 'hosts', 'headers', 'paths'" .. + (flavor == "expressions" and ", 'expression'" or "") .. " when 'protocols' is 'http'" } }, errs) @@ -1099,7 +1105,8 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function assert.falsy(ok) assert.same({ ["@entity"] = { - "must set one of 'methods', 'hosts', 'headers', 'paths', 'snis' when 'protocols' is 'https'" + "must set one of 'methods', 'hosts', 'headers', 'paths', 'snis'" .. + (flavor == "expressions" and ", 'expression'" or "") .. " when 'protocols' is 'https'" } }, errs) end) @@ -1114,7 +1121,8 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function assert.falsy(ok) assert.same({ ["@entity"] = { - "must set one of 'hosts', 'headers', 'paths' when 'protocols' is 'grpc'" + "must set one of 'hosts', 'headers', 'paths'" .. + (flavor == "expressions" and ", 'expression'" or "") .." when 'protocols' is 'grpc'" } }, errs) @@ -1126,7 +1134,8 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function assert.falsy(ok) assert.same({ ["@entity"] = { - "must set one of 'hosts', 'headers', 'paths', 'snis' when 'protocols' is 'grpcs'" + "must set one of 'hosts', 'headers', 'paths', 'snis'" .. + (flavor == "expressions" and ", 'expression'" or "") .. " when 'protocols' is 'grpcs'" } }, errs) end) @@ -1264,6 +1273,7 @@ describe("routes schema (flavor = traditional/traditional_compatible)", function end) end) +end -- for flavor describe("routes schema (flavor = expressions)", function() @@ -1273,7 +1283,7 @@ describe("routes schema (flavor = expressions)", function() reload_flavor("expressions") setup_global_env() - it("validates a valid route", function() + it("validates a valid route with only expression field", function() local route = { id = a_valid_uuid, name = "my_route", @@ -1292,6 +1302,65 @@ describe("routes schema (flavor = expressions)", function() assert.falsy(route.strip_path) end) + it("validates a valid route without expression field", function() + local route = { + id = a_valid_uuid, + name = "my_route", + protocols = { "https" }, + + methods = { "GET", "POST" }, + hosts = { "example.com" }, + headers = { location = { "location-1" } }, + paths = { "/ovo" }, + + snis = { "example.org" }, + sources = {{ ip = "127.0.0.1" }}, + destinations = {{ ip = "127.0.0.1" }}, + + strip_path = false, + preserve_host = true, + service = { id = another_uuid }, + } + route = Routes:process_auto_fields(route, "insert") + assert.truthy(route.created_at) + assert.truthy(route.updated_at) + assert.same(route.created_at, route.updated_at) + assert.truthy(Routes:validate(route)) + assert.falsy(route.strip_path) + end) + + it("fails when set 'expression' and others simultaneously", function() + local route = { + id = a_valid_uuid, + name = "my_route", + protocols = { "http" }, + expression = [[(http.method == "GET")]], + service = { id = another_uuid }, + } + + local others = { + methods = { "GET", "POST" }, + hosts = { "example.com" }, + headers = { location = { "location-1" } }, + paths = { "/ovo" }, + + snis = { "example.org" }, + sources = {{ ip = "127.0.0.1" }}, + destinations = {{ ip = "127.0.0.1" }}, + } + + for k, v in pairs(others) do + route[k] = v + + local r = Routes:process_auto_fields(route, "insert") + local ok, errs = Routes:validate_insert(r) + assert.falsy(ok) + assert.truthy(errs["@entity"]) + + route[k] = nil + end + end) + it("fails when priority is missing", function() local route = { priority = ngx.null } route = Routes:process_auto_fields(route, "insert") @@ -1300,12 +1369,20 @@ describe("routes schema (flavor = expressions)", function() assert.truthy(errs["priority"]) end) - it("fails when expression is missing", function() + it("fails when priority is more than 2^46 - 1", function() + local route = { priority = 2^46 } + route = Routes:process_auto_fields(route, "insert") + local ok, errs = Routes:validate_insert(route) + assert.falsy(ok) + assert.truthy(errs["priority"]) + end) + + it("fails when all fields is missing", function() local route = { expression = ngx.null } route = Routes:process_auto_fields(route, "insert") local ok, errs = Routes:validate_insert(route) assert.falsy(ok) - assert.truthy(errs["expression"]) + assert.truthy(errs["@entity"]) end) it("fails given an invalid expression", function() @@ -1322,11 +1399,12 @@ describe("routes schema (flavor = expressions)", function() end) -describe("routes schema (flavor = traditional_compatible)", function() +for _, flavor in ipairs({ "traditional_compatible", "expressions" }) do +describe("routes schema (flavor = " .. flavor .. ")", function() local a_valid_uuid = "cbb297c0-a956-486d-ad1d-f9b42df9465a" local another_uuid = "64a8670b-900f-44e7-a900-6ec7ef5aa4d3" - reload_flavor("traditional_compatible") + reload_flavor(flavor) setup_global_env() it("validates a valid http route", function() @@ -1416,6 +1494,7 @@ describe("routes schema (flavor = traditional_compatible)", function() assert.is_nil(errs) end) end) +end -- flavor in ipairs({ "traditional_compatible", "expressions" }) describe("routes schema (flavor = expressions)", function() @@ -1492,7 +1571,6 @@ describe("routes schema (flavor = expressions)", function() local ok, errs = Routes:validate_insert(route) assert.falsy(ok) - -- verified by `schema/typedefs.lua` assert.truthy(errs["@entity"]) end) @@ -1509,7 +1587,6 @@ describe("routes schema (flavor = expressions)", function() local ok, errs = Routes:validate_insert(route) assert.falsy(ok) - -- verified by `schema/typedefs.lua` assert.truthy(errs["@entity"]) end) @@ -1526,7 +1603,6 @@ describe("routes schema (flavor = expressions)", function() local ok, errs = Routes:validate_insert(route) assert.falsy(ok) - -- verified by `schema/typedefs.lua` assert.truthy(errs["@entity"]) end) diff --git a/spec/01-unit/08-router_spec.lua b/spec/01-unit/08-router_spec.lua index 913e109cbdcc..86f28ac33ab3 100644 --- a/spec/01-unit/08-router_spec.lua +++ b/spec/01-unit/08-router_spec.lua @@ -22,16 +22,6 @@ local function reload_router(flavor, subsystem) end local function new_router(cases, old_router) - -- add fields expression/priority only for flavor expressions - if kong.configuration.router_flavor == "expressions" then - for _, v in ipairs(cases) do - local r = v.route - - r.expression = r.expression or atc_compat.get_expression(r) - r.priority = r.priority or atc_compat._get_priority(r) - end - end - return Router.new(cases, nil, nil, old_router) end @@ -4878,7 +4868,7 @@ end) end -- for flavor -for _, flavor in ipairs({ "traditional", "traditional_compatible" }) do +for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor) @@ -4995,8 +4985,7 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible" }) do end) end -do - local flavor = "traditional_compatible" +for _, flavor in ipairs({ "traditional_compatible", "expressions" }) do describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor) @@ -5139,7 +5128,8 @@ do assert.same(use_case[2].route, match_t.route) end) end) -end -- local flavor = "traditional_compatible" +end -- for flavor + do local flavor = "expressions" @@ -5770,5 +5760,96 @@ do assert.falsy(match_t) end) end) + + describe("Router (flavor = " .. flavor .. ") [http]", function() + reload_router(flavor) + + it("rejects other fields if expression field exists", function() + local use_case = { + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", + paths = { "/foo" }, -- rejected + expression = [[http.path ^= r#"/bar"#]], -- effective + }, + }, + } + + local router = assert(new_router(use_case)) + + local match_t = router:select("GET", "/foo") + assert.falsy(match_t) + + local match_t = router:select("GET", "/bar") + assert.truthy(match_t) + end) + + it("expression route has higher priority than traditional route", function() + local use_case = { + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", + paths = { "/foo" }, + }, + }, + } + + local router = assert(new_router(use_case)) + + local match_t = router:select("GET", "/foo/bar") + assert.truthy(match_t) + assert.same(use_case[1].route, match_t.route) + + table.insert(use_case, { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", + expression = [[http.path ^= r#"/foo"#]], + }, + }) + + local router = assert(new_router(use_case)) + + local match_t = router:select("GET", "/foo/bar") + assert.truthy(match_t) + assert.same(use_case[2].route, match_t.route) + end) + + it("works when route.priority is near 2^46", function() + local use_case = { + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", + expression = [[http.path ^= r#"/foo"#]], + priority = 2^46 - 3, + }, + }, + } + + local router = assert(new_router(use_case)) + + local match_t = router:select("GET", "/foo/bar") + assert.truthy(match_t) + assert.same(use_case[1].route, match_t.route) + + table.insert(use_case, { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", + expression = [[http.path ^= r#"/foo"#]], + priority = 2^46 - 2, + }, + }) + + local router = assert(new_router(use_case)) + + local match_t = router:select("GET", "/foo/bar") + assert.truthy(match_t) + assert.same(use_case[2].route, match_t.route) + end) + end) end -- local flavor = "expressions" diff --git a/spec/02-integration/05-proxy/01-proxy_spec.lua b/spec/02-integration/05-proxy/01-proxy_spec.lua index c510b247532c..110c90cb423f 100644 --- a/spec/02-integration/05-proxy/01-proxy_spec.lua +++ b/spec/02-integration/05-proxy/01-proxy_spec.lua @@ -2,7 +2,6 @@ local helpers = require "spec.helpers" local utils = require "pl.utils" local stringx = require "pl.stringx" local http = require "resty.http" -local atc_compat = require "kong.router.compat" local function count_server_blocks(filename) @@ -162,16 +161,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.destinations = nil - return r end diff --git a/spec/02-integration/05-proxy/02-router_spec.lua b/spec/02-integration/05-proxy/02-router_spec.lua index 74d4f491bee3..ba32c8e6846d 100644 --- a/spec/02-integration/05-proxy/02-router_spec.lua +++ b/spec/02-integration/05-proxy/02-router_spec.lua @@ -122,8 +122,7 @@ local function remove_routes(strategy, routes) admin_api.plugins:remove(enable_buffering_plugin) end ---for _, flavor in ipairs({ "traditional", "traditional_compatible" }) do -for _, flavor in ipairs({ "traditional", "traditional_compatible" }) do +for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do for _, b in ipairs({ false, true }) do enable_buffering = b for _, strategy in helpers.each_strategy() do describe("Router [#" .. strategy .. ", flavor = " .. flavor .. "] with buffering [" .. (b and "on]" or "off]") , function() @@ -2596,6 +2595,76 @@ do end) + describe("Router [#" .. strategy .. ", flavor = " .. flavor .. "]", function() + local proxy_client + + reload_router(flavor) + + lazy_setup(function() + local bp = helpers.get_db_utils(strategy, { + "routes", + "services", + }) + + local service = bp.services:insert { + name = "global-cert", + } + + bp.routes:insert { + protocols = { "http" }, + expression = [[http.path == "/foo/bar"]], + priority = 2^46 - 1, + service = service, + } + + assert(helpers.start_kong({ + router_flavor = flavor, + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + allow_debug_header = true, + })) + end) + + lazy_teardown(function() + helpers.stop_kong() + end) + + before_each(function() + proxy_client = helpers.proxy_client() + end) + + after_each(function() + if proxy_client then + proxy_client:close() + end + end) + + it("can set route.priority to 2^46 - 1", function() + local res = assert(proxy_client:send { + method = "GET", + path = "/foo/bar", + headers = { ["kong-debug"] = 1 }, + }) + assert.res_status(200, res) + + local route_id = res.headers["kong-route-id"] + + local admin_client = helpers.admin_client() + local res = assert(admin_client:send { + method = "GET", + path = "/routes/" .. route_id, + }) + local body = assert.response(res).has_status(200) + assert(string.find(body, [["priority":70368744177663]])) + + local json = cjson.decode(body) + assert.equal(2^46 - 1, json.priority) + + admin_client:close() + end) + + end) + end -- strategy end -- http expression 'http.queries.*' diff --git a/spec/02-integration/05-proxy/06-ssl_spec.lua b/spec/02-integration/05-proxy/06-ssl_spec.lua index f03cae5bb23b..770300354419 100644 --- a/spec/02-integration/05-proxy/06-ssl_spec.lua +++ b/spec/02-integration/05-proxy/06-ssl_spec.lua @@ -2,7 +2,6 @@ local ssl_fixtures = require "spec.fixtures.ssl" local helpers = require "spec.helpers" local cjson = require "cjson" local fmt = string.format -local atc_compat = require "kong.router.compat" local function get_cert(server_name) @@ -62,18 +61,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.hosts = nil - r.paths = nil - r.snis = nil - return r end diff --git a/spec/02-integration/05-proxy/10-balancer/06-stream_spec.lua b/spec/02-integration/05-proxy/10-balancer/06-stream_spec.lua index 53dc73de26fa..b898979d6ed4 100644 --- a/spec/02-integration/05-proxy/10-balancer/06-stream_spec.lua +++ b/spec/02-integration/05-proxy/10-balancer/06-stream_spec.lua @@ -1,5 +1,4 @@ local helpers = require "spec.helpers" -local atc_compat = require "kong.router.compat" local function reload_router(flavor) @@ -24,16 +23,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.destinations = nil - return r end diff --git a/spec/02-integration/05-proxy/18-upstream_tls_spec.lua b/spec/02-integration/05-proxy/18-upstream_tls_spec.lua index df51053ffb0f..67bfdce3767d 100644 --- a/spec/02-integration/05-proxy/18-upstream_tls_spec.lua +++ b/spec/02-integration/05-proxy/18-upstream_tls_spec.lua @@ -1,6 +1,5 @@ local helpers = require "spec.helpers" local ssl_fixtures = require "spec.fixtures.ssl" -local atc_compat = require "kong.router.compat" local other_ca_cert = [[ @@ -103,20 +102,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.hosts = nil - r.paths = nil - r.snis = nil - - r.destinations = nil - return r end @@ -135,7 +122,7 @@ local function gen_plugin(route) end -for _, flavor in ipairs({ "traditional", "traditional_compatible" }) do +for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do for _, strategy in helpers.each_strategy() do describe("overriding upstream TLS parameters for database [#" .. strategy .. ", flavor = " .. flavor .. "]", function() local admin_client diff --git a/spec/02-integration/05-proxy/19-grpc_proxy_spec.lua b/spec/02-integration/05-proxy/19-grpc_proxy_spec.lua index 07ea00861dff..bc578ffd744e 100644 --- a/spec/02-integration/05-proxy/19-grpc_proxy_spec.lua +++ b/spec/02-integration/05-proxy/19-grpc_proxy_spec.lua @@ -1,7 +1,6 @@ local helpers = require "spec.helpers" local cjson = require "cjson" local pl_path = require "pl.path" -local atc_compat = require "kong.router.compat" local FILE_LOG_PATH = os.tmpname() @@ -28,18 +27,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.hosts = nil - r.paths = nil - r.snis = nil - return r end diff --git a/spec/02-integration/05-proxy/21-grpc_plugins_triggering_spec.lua b/spec/02-integration/05-proxy/21-grpc_plugins_triggering_spec.lua index 1dc731df5604..daaedb55d92c 100644 --- a/spec/02-integration/05-proxy/21-grpc_plugins_triggering_spec.lua +++ b/spec/02-integration/05-proxy/21-grpc_plugins_triggering_spec.lua @@ -1,6 +1,5 @@ local helpers = require "spec.helpers" local pl_file = require "pl.file" -local atc_compat = require "kong.router.compat" local TEST_CONF = helpers.test_conf @@ -28,18 +27,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.hosts = nil - r.paths = nil - r.snis = nil - return r end diff --git a/spec/02-integration/05-proxy/23-context_spec.lua b/spec/02-integration/05-proxy/23-context_spec.lua index 603dd21ee75e..f5e82e9e45ef 100644 --- a/spec/02-integration/05-proxy/23-context_spec.lua +++ b/spec/02-integration/05-proxy/23-context_spec.lua @@ -1,6 +1,5 @@ local helpers = require "spec.helpers" local null = ngx.null -local atc_compat = require "kong.router.compat" local function reload_router(flavor) @@ -25,17 +24,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.paths = nil - r.destinations = nil - return r end diff --git a/spec/02-integration/05-proxy/26-udp_spec.lua b/spec/02-integration/05-proxy/26-udp_spec.lua index 4be4717032e9..f0e342938223 100644 --- a/spec/02-integration/05-proxy/26-udp_spec.lua +++ b/spec/02-integration/05-proxy/26-udp_spec.lua @@ -1,5 +1,4 @@ local helpers = require "spec.helpers" -local atc_compat = require "kong.router.compat" local UDP_PROXY_PORT = 26001 @@ -27,16 +26,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.sources = nil - return r end diff --git a/spec/02-integration/05-proxy/28-stream_plugins_triggering_spec.lua b/spec/02-integration/05-proxy/28-stream_plugins_triggering_spec.lua index 927497446c8c..3bab5d24f02f 100644 --- a/spec/02-integration/05-proxy/28-stream_plugins_triggering_spec.lua +++ b/spec/02-integration/05-proxy/28-stream_plugins_triggering_spec.lua @@ -1,7 +1,6 @@ local helpers = require "spec.helpers" local pl_file = require "pl.file" local cjson = require "cjson" -local atc_compat = require "kong.router.compat" local TEST_CONF = helpers.test_conf @@ -97,16 +96,8 @@ local function reload_router(flavor) end +-- TODO: remove it when we confirm it is not needed local function gen_route(flavor, r) - if flavor ~= "expressions" then - return r - end - - r.expression = atc_compat.get_expression(r) - r.priority = tonumber(atc_compat._get_priority(r)) - - r.destinations = nil - return r end diff --git a/spec/02-integration/05-proxy/31-stream_tls_spec.lua b/spec/02-integration/05-proxy/31-stream_tls_spec.lua index 17a2897e68cc..3df56e1e7eeb 100644 --- a/spec/02-integration/05-proxy/31-stream_tls_spec.lua +++ b/spec/02-integration/05-proxy/31-stream_tls_spec.lua @@ -1,6 +1,6 @@ local helpers = require "spec.helpers" -for _, flavor in ipairs({ "traditional", "traditional_compatible" }) do +for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do for _, strategy in helpers.each_strategy({"postgres"}) do describe("#stream Proxying [#" .. strategy .. "] [#" .. flavor .. "]", function() local bp