Skip to content

Commit

Permalink
feat(db/schema): add traditional_compatible fields to expressions
Browse files Browse the repository at this point in the history
… 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 <[email protected]>
  • Loading branch information
chronolaw and dndx authored Apr 17, 2024
1 parent 41628c3 commit 00d1c3f
Show file tree
Hide file tree
Showing 20 changed files with 545 additions and 367 deletions.
Original file line number Diff line number Diff line change
@@ -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
220 changes: 100 additions & 120 deletions kong/db/schema/entities/routes.lua
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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,
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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
55 changes: 42 additions & 13 deletions kong/db/schema/entities/routes_subschemas.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
6 changes: 6 additions & 0 deletions kong/router/atc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

1 comment on commit 00d1c3f

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Bazel Build

Docker image available kong/kong:00d1c3ff8afddb1b9c2600701d00c7655ef9f828
Artifacts available https://github.com/Kong/kong/actions/runs/8723891492

Please sign in to comment.