Skip to content

Commit 0d98bb6

Browse files
committed
feat: Add dependency protocol checking and deletion checking for stream routing
Signed-off-by: Orician <[email protected]>
1 parent 896d3c3 commit 0d98bb6

File tree

2 files changed

+329
-0
lines changed

2 files changed

+329
-0
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: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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 code, body = t('/apisix/admin/stream_routes/3',
108+
ngx.HTTP_PUT,
109+
[[{
110+
"protocol": {"name": "redis", "superior_id": "999"},
111+
"upstream": {
112+
"nodes": {"127.0.0.1:6381": 1},
113+
"type": "roundrobin"
114+
}
115+
}]]
116+
)
117+
if code ~= 400 then
118+
ngx.say("failed: expected 400, got ", code)
119+
return
120+
end
121+
if not body or not string.find(body, "failed to fetch stream routes[999]", 1, true) then
122+
ngx.say("failed: unexpected body: ", body)
123+
return
124+
end
125+
ngx.say("passed")
126+
}
127+
}
128+
--- request
129+
GET /t
130+
--- response_body
131+
passed
132+
133+
134+
135+
=== TEST 4: protocol mismatch (should fail)
136+
--- config
137+
location /t {
138+
content_by_lua_block {
139+
local t = require("lib.test_admin").test
140+
local code = t('/apisix/admin/stream_routes/4',
141+
ngx.HTTP_PUT,
142+
[[{
143+
"protocol": {"name": "dubbo"},
144+
"upstream": {
145+
"nodes": {"127.0.0.1:20880": 1},
146+
"type": "roundrobin"
147+
}
148+
}]]
149+
)
150+
151+
local code, body = t('/apisix/admin/stream_routes/5',
152+
ngx.HTTP_PUT,
153+
[[{
154+
"protocol": {"name": "redis", "superior_id": "4"},
155+
"upstream": {
156+
"nodes": {"127.0.0.1:6382": 1},
157+
"type": "roundrobin"
158+
}
159+
}]]
160+
)
161+
if code ~= 400 then
162+
ngx.say("failed: expected 400, got ", code)
163+
return
164+
end
165+
if not body or not string.find(body, "protocol mismatch", 1, true) then
166+
ngx.say("failed: unexpected body: ", body)
167+
return
168+
end
169+
ngx.say("passed")
170+
}
171+
}
172+
--- request
173+
GET /t
174+
--- response_body
175+
passed
176+
177+
178+
179+
=== TEST 5: delete superior route being referenced (should fail)
180+
--- config
181+
location /t {
182+
content_by_lua_block {
183+
local t = require("lib.test_admin").test
184+
local code, body = t('/apisix/admin/stream_routes/1',
185+
ngx.HTTP_DELETE
186+
)
187+
if code ~= 400 then
188+
ngx.say("failed: expected 400, got ", code)
189+
return
190+
end
191+
if not body or not string.find(body, "can not delete this stream route", 1, true) then
192+
ngx.say("failed: unexpected body: ", body)
193+
return
194+
end
195+
ngx.say("passed")
196+
}
197+
}
198+
--- request
199+
GET /t
200+
--- response_body
201+
passed
202+
203+
204+
205+
=== TEST 6: delete subordinate route first
206+
--- config
207+
location /t {
208+
content_by_lua_block {
209+
local t = require("lib.test_admin").test
210+
local code, body = t('/apisix/admin/stream_routes/2',
211+
ngx.HTTP_DELETE
212+
)
213+
if code >= 300 then
214+
ngx.status = code
215+
end
216+
ngx.say(body)
217+
}
218+
}
219+
--- request
220+
GET /t
221+
--- response_body
222+
passed
223+
224+
225+
226+
=== TEST 7: now delete superior route should succeed
227+
--- config
228+
location /t {
229+
content_by_lua_block {
230+
local t = require("lib.test_admin").test
231+
local code, body = t('/apisix/admin/stream_routes/1',
232+
ngx.HTTP_DELETE
233+
)
234+
if code >= 300 then
235+
ngx.status = code
236+
end
237+
ngx.say(body)
238+
}
239+
}
240+
--- request
241+
GET /t
242+
--- response_body
243+
passed
244+
245+
246+
247+
=== TEST 8: cleanup
248+
--- config
249+
location /t {
250+
content_by_lua_block {
251+
local t = require("lib.test_admin").test
252+
t('/apisix/admin/stream_routes/4', ngx.HTTP_DELETE)
253+
t('/apisix/admin/stream_routes/5', ngx.HTTP_DELETE)
254+
ngx.say("passed")
255+
}
256+
}
257+
--- request
258+
GET /t
259+
--- response_body
260+
passed

0 commit comments

Comments
 (0)