Skip to content

Commit e9d194a

Browse files
authored
feat: Add dependency protocol checking and deletion checking for stream routing (#12794)
1 parent baa0642 commit e9d194a

File tree

3 files changed

+333
-2
lines changed

3 files changed

+333
-2
lines changed

apisix/admin/stream_routes.lua

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
local core = require("apisix.core")
1818
local resource = require("apisix.admin.resource")
1919
local stream_route_checker = require("apisix.stream.router.ip_port").stream_route_checker
20+
local tostring = tostring
21+
local ipairs = ipairs
22+
local type = type
2023

2124

2225
local function check_conf(id, conf, need_id, schema, opts)
@@ -60,6 +63,33 @@ local function check_conf(id, conf, need_id, schema, opts)
6063
end
6164
end
6265

66+
if conf.protocol and conf.protocol.superior_id and not opts.skip_references_check then
67+
local superior_id = conf.protocol.superior_id
68+
local key = "/stream_routes/" .. superior_id
69+
local res, err = core.etcd.get(key)
70+
if not res then
71+
return nil, {error_msg = "failed to fetch stream routes[" .. superior_id .. "]: "
72+
.. err}
73+
end
74+
75+
if res.status ~= 200 then
76+
return nil, {error_msg = "failed to fetch stream routes[" .. superior_id
77+
.. "], response code: " .. res.status}
78+
end
79+
80+
local superior_route = res.body.node.value
81+
if type(superior_route) == "string" then
82+
superior_route = core.json.decode(superior_route)
83+
end
84+
85+
if superior_route and superior_route.protocol
86+
and superior_route.protocol.name ~= conf.protocol.name then
87+
return nil, {error_msg = "protocol mismatch: subordinate protocol ["
88+
.. conf.protocol.name .. "] does not match superior protocol ["
89+
.. superior_route.protocol.name .. "]"}
90+
end
91+
end
92+
6393
local ok, err = stream_route_checker(conf, true)
6494
if not ok then
6595
return nil, {error_msg = err}
@@ -69,11 +99,50 @@ local function check_conf(id, conf, need_id, schema, opts)
6999
end
70100

71101

102+
local function delete_checker(id)
103+
local key = "/stream_routes"
104+
local res, err = core.etcd.get(key, {prefix = true})
105+
if not res then
106+
return nil, {error_msg = "failed to fetch stream routes: " .. err}
107+
end
108+
109+
if res.status ~= 200 then
110+
return nil, {error_msg = "failed to fetch stream routes, response code: " .. res.status}
111+
end
112+
113+
local nodes = res.body.list
114+
if not nodes then
115+
if res.body.node and res.body.node.nodes then
116+
nodes = res.body.node.nodes
117+
end
118+
end
119+
120+
if not nodes then
121+
return true
122+
end
123+
124+
for _, item in ipairs(nodes) do
125+
local route = item.value
126+
if type(route) == "string" then
127+
route = core.json.decode(route)
128+
end
129+
130+
if route and route.protocol and tostring(route.protocol.superior_id) == id then
131+
return 400, {error_msg = "can not delete this stream route directly, stream route ["
132+
.. route.id .. "] is still using it as superior_id"}
133+
end
134+
end
135+
136+
return true
137+
end
138+
139+
72140
return resource.new({
73141
name = "stream_routes",
74142
kind = "stream route",
75143
schema = core.schema.stream_route,
76144
checker = check_conf,
145+
delete_checker = delete_checker,
77146
unsupported_methods = { "patch" },
78147
list_filter_fields = {
79148
service_id = true,
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
use t::APISIX 'no_plan';
18+
19+
repeat_each(1);
20+
no_long_string();
21+
no_root_location();
22+
23+
add_block_preprocessor(sub {
24+
my ($block) = @_;
25+
26+
if (!$block->extra_yaml_config) {
27+
my $extra_yaml_config = <<_EOC_;
28+
xrpc:
29+
protocols:
30+
- name: redis
31+
- name: dubbo
32+
_EOC_
33+
$block->set_value("extra_yaml_config", $extra_yaml_config);
34+
}
35+
36+
$block;
37+
});
38+
39+
run_tests;
40+
41+
__DATA__
42+
43+
=== TEST 1: create superior route
44+
--- config
45+
location /t {
46+
content_by_lua_block {
47+
local t = require("lib.test_admin").test
48+
local code, body = t('/apisix/admin/stream_routes/1',
49+
ngx.HTTP_PUT,
50+
[[{
51+
"protocol": {"name": "redis"},
52+
"upstream": {
53+
"nodes": {"127.0.0.1:6379": 1},
54+
"type": "roundrobin"
55+
}
56+
}]]
57+
)
58+
if code >= 300 then
59+
ngx.status = code
60+
end
61+
ngx.say(body)
62+
}
63+
}
64+
--- request
65+
GET /t
66+
--- response_body
67+
passed
68+
69+
70+
71+
=== TEST 2: create subordinate route with valid superior_id
72+
--- config
73+
location /t {
74+
content_by_lua_block {
75+
local t = require("lib.test_admin").test
76+
local code, body = t('/apisix/admin/stream_routes/2',
77+
ngx.HTTP_PUT,
78+
[[{
79+
"protocol": {
80+
"name": "redis",
81+
"superior_id": "1"
82+
},
83+
"upstream": {
84+
"nodes": {"127.0.0.1:6380": 1},
85+
"type": "roundrobin"
86+
}
87+
}]]
88+
)
89+
if code >= 300 then
90+
ngx.status = code
91+
end
92+
ngx.say(body)
93+
}
94+
}
95+
--- request
96+
GET /t
97+
--- response_body
98+
passed
99+
100+
101+
102+
=== TEST 3: superior_id not exist (should fail)
103+
--- config
104+
location /t {
105+
content_by_lua_block {
106+
local t = require("lib.test_admin").test
107+
local json = require("toolkit.json")
108+
local code, body = t('/apisix/admin/stream_routes/3',
109+
ngx.HTTP_PUT,
110+
[[{
111+
"protocol": {"name": "redis", "superior_id": "999"},
112+
"upstream": {
113+
"nodes": {"127.0.0.1:6381": 1},
114+
"type": "roundrobin"
115+
}
116+
}]]
117+
)
118+
if code ~= 400 then
119+
ngx.say("failed: expected 400, got ", code)
120+
return
121+
end
122+
local data = json.decode(body)
123+
if not data or not data.error_msg then
124+
ngx.say("failed: unexpected body: ", body)
125+
return
126+
end
127+
if not string.find(data.error_msg, "failed to fetch stream routes[999]", 1, true) then
128+
ngx.say("failed: unexpected body: ", body)
129+
return
130+
end
131+
ngx.say("passed")
132+
}
133+
}
134+
--- request
135+
GET /t
136+
--- response_body
137+
passed
138+
139+
140+
141+
=== TEST 4: protocol mismatch (should fail)
142+
--- config
143+
location /t {
144+
content_by_lua_block {
145+
local t = require("lib.test_admin").test
146+
local json = require("toolkit.json")
147+
local code = t('/apisix/admin/stream_routes/4',
148+
ngx.HTTP_PUT,
149+
[[{
150+
"protocol": {"name": "dubbo"},
151+
"upstream": {
152+
"nodes": {"127.0.0.1:20880": 1},
153+
"type": "roundrobin"
154+
}
155+
}]]
156+
)
157+
158+
local code, body = t('/apisix/admin/stream_routes/5',
159+
ngx.HTTP_PUT,
160+
[[{
161+
"protocol": {"name": "redis", "superior_id": "4"},
162+
"upstream": {
163+
"nodes": {"127.0.0.1:6382": 1},
164+
"type": "roundrobin"
165+
}
166+
}]]
167+
)
168+
if code ~= 400 then
169+
ngx.say("failed: expected 400, got ", code)
170+
return
171+
end
172+
local data = json.decode(body)
173+
if not data or not data.error_msg then
174+
ngx.say("failed: unexpected body: ", body)
175+
return
176+
end
177+
if not string.find(data.error_msg, "protocol mismatch", 1, true) then
178+
ngx.say("failed: unexpected body: ", body)
179+
return
180+
end
181+
ngx.say("passed")
182+
}
183+
}
184+
--- request
185+
GET /t
186+
--- response_body
187+
passed
188+
189+
190+
191+
=== TEST 5: delete superior route being referenced (should fail)
192+
--- config
193+
location /t {
194+
content_by_lua_block {
195+
local t = require("lib.test_admin").test
196+
local json = require("toolkit.json")
197+
local code, body = t('/apisix/admin/stream_routes/1',
198+
ngx.HTTP_DELETE
199+
)
200+
if code ~= 400 then
201+
ngx.say("failed: expected 400, got ", code)
202+
return
203+
end
204+
local data = json.decode(body)
205+
if not data or not data.error_msg then
206+
ngx.say("failed: unexpected body: ", body)
207+
return
208+
end
209+
if not string.find(data.error_msg, "can not delete this stream route", 1, true) then
210+
ngx.say("failed: unexpected body: ", body)
211+
return
212+
end
213+
ngx.say("passed")
214+
}
215+
}
216+
--- request
217+
GET /t
218+
--- response_body
219+
passed
220+
221+
222+
223+
=== TEST 6: delete subordinate route first
224+
--- config
225+
location /t {
226+
content_by_lua_block {
227+
local t = require("lib.test_admin").test
228+
local code, body = t('/apisix/admin/stream_routes/2',
229+
ngx.HTTP_DELETE
230+
)
231+
if code >= 300 then
232+
ngx.status = code
233+
end
234+
ngx.say(body)
235+
}
236+
}
237+
--- request
238+
GET /t
239+
--- response_body
240+
passed
241+
242+
243+
244+
=== TEST 7: now delete superior route should succeed
245+
--- config
246+
location /t {
247+
content_by_lua_block {
248+
local t = require("lib.test_admin").test
249+
local code, body = t('/apisix/admin/stream_routes/1',
250+
ngx.HTTP_DELETE
251+
)
252+
if code >= 300 then
253+
ngx.status = code
254+
end
255+
ngx.say(body)
256+
}
257+
}
258+
--- request
259+
GET /t
260+
--- response_body
261+
passed

t/xrpc/pingpong.t

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,9 +510,10 @@ call pingpong's log, ctx unfinished: false
510510
}
511511
}
512512
)
513-
if code >= 300 then
513+
-- Verify that invalid superior_id returns 400 error instead of any other response code
514+
if code ~= 400 then
514515
ngx.status = code
515-
ngx.say(body)
516+
ngx.say("expected 400 for invalid superior_id, got " .. code .. ": " .. body)
516517
return
517518
end
518519

0 commit comments

Comments
 (0)