From 93cf96dda658c669e6e7c384b912ee109494e825 Mon Sep 17 00:00:00 2001 From: spacewander Date: Fri, 3 Sep 2021 10:15:31 +0800 Subject: [PATCH] feat: allow route to inherit hosts from service Signed-off-by: spacewander --- apisix/http/route.lua | 15 +- apisix/http/router/radixtree_host_uri.lua | 31 +- apisix/http/router/radixtree_uri.lua | 12 +- .../router/radixtree_uri_with_parameter.lua | 12 +- apisix/plugin.lua | 7 + apisix/schema_def.lua | 7 +- docs/en/latest/admin-api.md | 4 +- docs/zh/latest/admin-api.md | 4 +- t/router/radixtree-host-uri2.t | 36 +++ t/router/radixtree-uri-host.t | 289 ++++++++++++++++++ t/router/radixtree-uri-with-parameter.t | 87 ++++++ 11 files changed, 489 insertions(+), 15 deletions(-) diff --git a/apisix/http/route.lua b/apisix/http/route.lua index c5a5358c69ef3..b0b886dbd5ca4 100644 --- a/apisix/http/route.lua +++ b/apisix/http/route.lua @@ -17,6 +17,7 @@ local require = require local radixtree = require("resty.radixtree") local router = require("apisix.utils.router") +local service_fetch = require("apisix.http.service").get local core = require("apisix.core") local expr = require("resty.expr.v1") local plugin_checker = require("apisix.plugin").plugin_checker @@ -56,13 +57,25 @@ function _M.create_radixtree_uri_router(routes, uri_routes, with_parameter) filter_fun = filter_fun() end + local hosts = route.value.hosts or route.value.host + if not hosts and route.value.service_id then + local service = service_fetch(route.value.service_id) + if not service then + core.log.error("failed to fetch service configuration by ", + "id: ", route.value.service_id) + goto CONTINUE + end + + hosts = service.value.hosts + end + core.log.info("insert uri route: ", core.json.delay_encode(route.value, true)) core.table.insert(uri_routes, { paths = route.value.uris or route.value.uri, methods = route.value.methods, priority = route.value.priority, - hosts = route.value.hosts or route.value.host, + hosts = hosts, remote_addrs = route.value.remote_addrs or route.value.remote_addr, vars = route.value.vars, diff --git a/apisix/http/router/radixtree_host_uri.lua b/apisix/http/router/radixtree_host_uri.lua index 08e0655c7bee9..69979e465dd78 100644 --- a/apisix/http/router/radixtree_host_uri.lua +++ b/apisix/http/router/radixtree_host_uri.lua @@ -17,12 +17,15 @@ local require = require local router = require("apisix.utils.router") local core = require("apisix.core") +local get_services = require("apisix.http.service").services +local service_fetch = require("apisix.http.service").get local ipairs = ipairs local type = type local tab_insert = table.insert local loadstring = loadstring local pairs = pairs -local cached_version +local cached_router_version +local cached_service_version local host_router local only_uri_router @@ -49,7 +52,20 @@ local function push_host_router(route, host_routes, only_uri_routes) filter_fun = filter_fun() end - local hosts = route.value.hosts or {route.value.host} + local hosts = route.value.hosts + if not hosts then + if route.value.host then + hosts = {route.value.host} + elseif route.value.service_id then + local service = service_fetch(route.value.service_id) + if not service then + core.log.error("failed to fetch service configuration by ", + "id: ", route.value.service_id) + else + hosts = service.value.hosts + end + end + end local radixtree_route = { paths = route.value.uris or route.value.uri, @@ -66,7 +82,7 @@ local function push_host_router(route, host_routes, only_uri_routes) end } - if #hosts == 0 then + if hosts == nil then core.table.insert(only_uri_routes, radixtree_route) return end @@ -124,11 +140,16 @@ end local match_opts = {} function _M.match(api_ctx) local user_routes = _M.user_routes - if not cached_version or cached_version ~= user_routes.conf_version then + local _, service_version = get_services() + if not cached_router_version or cached_router_version ~= user_routes.conf_version + or not cached_service_version or cached_service_version ~= service_version + then create_radixtree_router(user_routes.values) - cached_version = user_routes.conf_version + cached_router_version = user_routes.conf_version + cached_service_version = service_version end + core.table.clear(match_opts) match_opts.method = api_ctx.var.request_method match_opts.remote_addr = api_ctx.var.remote_addr diff --git a/apisix/http/router/radixtree_uri.lua b/apisix/http/router/radixtree_uri.lua index f3245fdd27c1e..b438ed8d877d2 100644 --- a/apisix/http/router/radixtree_uri.lua +++ b/apisix/http/router/radixtree_uri.lua @@ -17,7 +17,9 @@ local require = require local core = require("apisix.core") local base_router = require("apisix.http.route") -local cached_version +local get_services = require("apisix.http.service").services +local cached_router_version +local cached_service_version local _M = {version = 0.2} @@ -28,10 +30,14 @@ local _M = {version = 0.2} local match_opts = {} function _M.match(api_ctx) local user_routes = _M.user_routes - if not cached_version or cached_version ~= user_routes.conf_version then + local _, service_version = get_services() + if not cached_router_version or cached_router_version ~= user_routes.conf_version + or not cached_service_version or cached_service_version ~= service_version + then uri_router = base_router.create_radixtree_uri_router(user_routes.values, uri_routes, false) - cached_version = user_routes.conf_version + cached_router_version = user_routes.conf_version + cached_service_version = service_version end if not uri_router then diff --git a/apisix/http/router/radixtree_uri_with_parameter.lua b/apisix/http/router/radixtree_uri_with_parameter.lua index f7b6a2243c758..257017199f27d 100644 --- a/apisix/http/router/radixtree_uri_with_parameter.lua +++ b/apisix/http/router/radixtree_uri_with_parameter.lua @@ -17,7 +17,9 @@ local require = require local core = require("apisix.core") local base_router = require("apisix.http.route") -local cached_version +local get_services = require("apisix.http.service").services +local cached_router_version +local cached_service_version local _M = {} @@ -28,10 +30,14 @@ local _M = {} local match_opts = {} function _M.match(api_ctx) local user_routes = _M.user_routes - if not cached_version or cached_version ~= user_routes.conf_version then + local _, service_version = get_services() + if not cached_router_version or cached_router_version ~= user_routes.conf_version + or not cached_service_version or cached_service_version ~= service_version + then uri_router = base_router.create_radixtree_uri_router(user_routes.values, uri_routes, true) - cached_version = user_routes.conf_version + cached_router_version = user_routes.conf_version + cached_service_version = service_version end if not uri_router then diff --git a/apisix/plugin.lua b/apisix/plugin.lua index c158011d294d3..ac2aed9285ee4 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -401,6 +401,13 @@ local function merge_service_route(service_conf, route_conf) new_conf.value.name = nil end + if route_conf.value.hosts then + new_conf.value.hosts = route_conf.value.hosts + end + if not new_conf.value.hosts and route_conf.value.host then + new_conf.value.host = route_conf.value.host + end + -- core.log.info("merged conf : ", core.json.delay_encode(new_conf)) return new_conf end diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index c8f4ad50f17a4..adddf155d8bfd 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -661,7 +661,12 @@ _M.service = { description = "enable websocket for request", type = "boolean", }, - + hosts = { + type = "array", + items = host_def, + minItems = 1, + uniqueItems = true, + }, }, } diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index 1d4e2e541934b..b6ce27b344df4 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -323,12 +323,13 @@ Return response from etcd currently. | desc | False | Auxiliary | service usage scenarios, and more. | service xxxx | | labels | False | Match Rules | Key/value pairs to specify attributes | {"version":"v2","build":"16","env":"production"} | | enable_websocket | False | Auxiliary | enable `websocket`(boolean), default `false`. | | +| hosts | False | Match Rules | The `host` in the form of a non-empty list means that multiple different hosts are allowed, and match any one of them.| ["foo.com", "*.bar.com"] | | create_time | False | Auxiliary | epoch timestamp in second, will be created automatically if missing | 1602883670 | | update_time | False | Auxiliary | epoch timestamp in second, will be created automatically if missing | 1602883670 | Config Example: -```shell +```json { "id": "1", # id "plugins": {}, # Bound plugin @@ -337,6 +338,7 @@ Config Example: "name": "service-test", "desc": "hello world", "enable_websocket": true, + "hosts": ["foo.com"] } ``` diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 6fdc37bc96191..0e809e1a948ff 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -325,12 +325,13 @@ HTTP/1.1 200 OK | desc | 可选 | 辅助 | 服务描述、使用场景等。 | | | labels | 可选 | 匹配规则 | 标识附加属性的键值对 | {"version":"v2","build":"16","env":"production"} | | enable_websocket | 可选 | 辅助 | 是否启用 `websocket`(boolean), 缺省 `false`. | | +| hosts | 可选 | 匹配规则 | 非空列表形态的 `host`,表示允许有多个不同 `host`,匹配其中任意一个即可。| ["foo.com", "\*.bar.com"] | | create_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 | | update_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 | service 对象 json 配置内容: -```shell +```json { "id": "1", # id "plugins": {}, # 指定 service 绑定的插件 @@ -339,6 +340,7 @@ service 对象 json 配置内容: "name": "测试svc", # service 名称 "desc": "hello world", # service 描述 "enable_websocket": true, #启动 websocket 功能 + "hosts": ["foo.com"] } ``` diff --git a/t/router/radixtree-host-uri2.t b/t/router/radixtree-host-uri2.t index 20f034bcfd49c..313bc6a1444f6 100644 --- a/t/router/radixtree-host-uri2.t +++ b/t/router/radixtree-host-uri2.t @@ -376,3 +376,39 @@ GET /server_port Host: test.com --- no_error_log [error] + + + +=== TEST 14: inherit hosts from services +--- yaml_config eval: $::yaml_config +--- apisix_yaml +services: + - id: 1 + hosts: + - bar.com +upstreams: + - id: 1 + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +routes: + - + service_id: 1 + upstream_id: 1 + uri: /hello + plugins: + proxy-rewrite: + uri: /hello1 + - + upstream_id: 1 + uri: /hello + priority: -1 +#END +--- more_headers +Host: www.foo.com +--- request +GET /hello +--- response_body +hello world +--- no_error_log +[error] diff --git a/t/router/radixtree-uri-host.t b/t/router/radixtree-uri-host.t index a0d1141225016..6e93ae48ca3ad 100644 --- a/t/router/radixtree-uri-host.t +++ b/t/router/radixtree-uri-host.t @@ -299,3 +299,292 @@ GET /file:xx --- error_code: 404 --- no_error_log [error] + + + +=== TEST 18: inherit hosts from services +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "hosts": ["bar.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "proxy-rewrite":{"uri":"/hello1"} + }, + "service_id": "1", + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello", + "priority": -1 + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 19: hit +--- more_headers +Host: www.foo.com +--- request +GET /hello +--- response_body +hello world +--- no_error_log +[error] + + + +=== TEST 20: change hosts in services +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "hosts": ["foo.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.sleep(0.1) + + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {Host = "foo.com"}}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "hosts": ["bar.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.sleep(0.1) + + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {Host = "foo.com"}}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + } + } +--- request +GET /t +--- response_body +hello1 world +hello world +--- no_error_log +[error] + + + +=== TEST 21: unbind services +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local t = require("lib.test_admin").test + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "proxy-rewrite":{"uri":"/hello1"} + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + ngx.sleep(0.1) + + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {Host = "foo.com"}}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + } + } +--- request +GET /t +--- response_body +hello1 world +--- no_error_log +[error] + + + +=== TEST 22: host from route is preferred +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local t = require("lib.test_admin").test + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "hosts": ["foo.com"], + "plugins": { + "proxy-rewrite":{"uri":"/hello1"} + }, + "service_id": "1", + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + ngx.sleep(0.1) + + for _, h in ipairs({"foo.com", "bar.com"}) do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {Host = h}}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "host": "foo.com", + "plugins": { + "proxy-rewrite":{"uri":"/hello1"} + }, + "service_id": "1", + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + ngx.sleep(0.1) + + for _, h in ipairs({"foo.com", "bar.com"}) do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {Host = h}}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + end + } + } +--- request +GET /t +--- response_body +hello1 world +hello world +hello1 world +hello world +--- no_error_log +[error] diff --git a/t/router/radixtree-uri-with-parameter.t b/t/router/radixtree-uri-with-parameter.t index 70fd59a5d3616..d8fa0950c235f 100644 --- a/t/router/radixtree-uri-with-parameter.t +++ b/t/router/radixtree-uri-with-parameter.t @@ -212,3 +212,90 @@ GET /json/bbb/foo {"error_msg":"404 Route Not Found"} --- no_error_log [error] + + + +=== TEST 10: inherit hosts from services +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "hosts": ["bar.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "proxy-rewrite":{"uri":"/hello1"} + }, + "service_id": "1", + "uri": "/:name/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "proxy-rewrite":{"uri":"/hello"} + }, + "uri": "/:name/hello", + "priority": -1 + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 11: hit +--- more_headers +Host: www.foo.com +--- request +GET /john/hello +--- response_body +hello world +--- no_error_log +[error]