diff --git a/src/apisix/Makefile b/src/apisix/Makefile index 087c7b5..8973287 100644 --- a/src/apisix/Makefile +++ b/src/apisix/Makefile @@ -20,12 +20,14 @@ test-busted: -v ${ROOT_DIR}/plugins:/bkgateway/apisix/plugins \ apisix-test-busted "/run-test-busted.sh" +# make test-nginx +# make test-nginx CASE_FILE=bk-traffic-label.t .PHONY: test-nginx test-nginx: @docker run --rm ${RUN_WITH_IT} \ -v ${ROOT_DIR}/t:/bkgateway/t/ \ -v ${ROOT_DIR}/plugins:/bkgateway/apisix/plugins \ - apisix-test-nginx "/run-test-nginx.sh" + apisix-test-nginx "/run-test-nginx.sh" $(if $(CASE_FILE),$(CASE_FILE)) .PHONY: apisix-test-images apisix-test-images: apisix-test-busted apisix-test-nginx diff --git a/src/apisix/ci/run-test-nginx.sh b/src/apisix/ci/run-test-nginx.sh index 2dd280d..7e966b7 100755 --- a/src/apisix/ci/run-test-nginx.sh +++ b/src/apisix/ci/run-test-nginx.sh @@ -71,5 +71,10 @@ export OPENRESTY_PREFIX="/usr/local/openresty-debug" export APISIX_MAIN="https://raw.githubusercontent.com/apache/incubator-apisix/master/rockspec/apisix-master-0.rockspec" export PATH=$OPENRESTY_PREFIX/nginx/sbin:$OPENRESTY_PREFIX/luajit/bin:$OPENRESTY_PREFIX/bin:$PATH -FLUSH_ETCD=1 prove --timer -Itest-nginx/lib -I./ t/bk-*.t +if [ -n "$1" ]; then + CASE_FILE=$1 + FLUSH_ETCD=1 prove --timer -Itest-nginx/lib -I./ t/bk-00.t t/$CASE_FILE +else + FLUSH_ETCD=1 prove --timer -Itest-nginx/lib -I./ t/bk-*.t +fi diff --git a/src/apisix/plugins/README.md b/src/apisix/plugins/README.md index ecfa077..33a859c 100644 --- a/src/apisix/plugins/README.md +++ b/src/apisix/plugins/README.md @@ -56,6 +56,7 @@ proxy 预处理:17000 ~ 17500 +- bk-traffic-label # priority: 17460 - bk-delete-sensitive # priority: 17450 - bk-delete-cookie # priority: 17440 - bk-proxy-rewrite # priority: 17430 # 该插件供 operator 进行后端地址转换使用 diff --git a/src/apisix/plugins/bk-traffic-label.lua b/src/apisix/plugins/bk-traffic-label.lua new file mode 100644 index 0000000..d85e719 --- /dev/null +++ b/src/apisix/plugins/bk-traffic-label.lua @@ -0,0 +1,153 @@ +-- +-- TencentBlueKing is pleased to support the open source community by making +-- 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +-- Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +-- Licensed under the MIT License (the "License"); you may not use this file except +-- in compliance with the License. You may obtain a copy of the License at +-- +-- http://opensource.org/licenses/MIT +-- +-- Unless required by applicable law or agreed to in writing, software distributed under +-- the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +-- either express or implied. See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- We undertake not to change the open source license (MIT license) applicable +-- to the current version of the project delivered to anyone in the future. +-- + +-- this plugin is impls based on the doc of api7 traffic-label +-- link: https://docs.api7.ai/hub/traffic-label/ + +local core = require("apisix.core") +local expr = require("resty.expr.v1") +local pairs = pairs +local ipairs = ipairs + +local match_schema = { + type = "array", +} + +local actions_schema = { + type = "array", + items = { + type = "object", + properties = { + set_headers = { + type = "object", + additionalProperties = { + type = "string" + } + }, + weight = { + description = "percentage of all matched which would do the actions", + type = "integer", + default = 1, + minimum = 0 + } + } + }, + minItems = 1, + maxItems = 20 +} + +local schema = { + type = "object", + properties = { + rules = { + type = "array", + items = { + type = "object", + properties = { + match = match_schema, + actions = actions_schema + }, + } + } + }, + required = {"rules"}, +} + +local plugin_name = "bk-traffic-label" + +local _M = { + version = 0.1, + priority = 17460, + name = plugin_name, + schema = schema +} + +function _M.check_schema(conf) + -- Validate the configuration schema + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + if conf.rules then + for _, rule in ipairs(conf.rules) do + if rule.match then + -- Validate the match expression + local _, err2 = expr.new(rule.match) + if err2 then + core.log.error("failed to validate the 'match' expression: ", err2) + return false, "failed to validate the 'match' expression: " .. err2 + end + end + + -- Calculate total weight of all actions and preprocess actions to set default weight + local total_weight = 0 + for _, action in ipairs(rule.actions) do + if action.weight == nil or action.weight < 0 then + action.weight = 1 + end + total_weight = total_weight + action.weight + end + rule.total_weight = total_weight + end + end + + return true +end + +local function apply_actions(rule, ctx) + -- Generate a random number between 1 and total_weight, [1, total_weight] + local random_weight = math.random(1, rule.total_weight) + local current_weight = 0 + + for _, action in ipairs(rule.actions) do + current_weight = current_weight + action.weight + -- Apply the action if the random number falls within the current weight range + if random_weight <= current_weight then + if action.set_headers then + -- Set the specified headers + for k, v in pairs(action.set_headers) do + core.request.set_header(ctx, k, v) + end + end + break + end + end +end + +function _M.access(conf, ctx) + if not conf or not conf.rules then + return + end + + for _, rule in ipairs(conf.rules) do + if rule.match then + -- Evaluate the match expression + local ex, _ = expr.new(rule.match) + local match_passed = ex:eval(ctx.var) + if match_passed then + -- Apply the actions if the match condition is met + apply_actions(rule, ctx) + end + end + end + + return +end + +return _M diff --git a/src/apisix/t/bk-00.t b/src/apisix/t/bk-00.t new file mode 100644 index 0000000..bbeb0f0 --- /dev/null +++ b/src/apisix/t/bk-00.t @@ -0,0 +1,54 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions and +# limitations under the License. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# + +# NOTE: this file should be ran as the first test file +# otherwise other test files would fail + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local ok = true + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +done diff --git a/src/apisix/t/bk-traffic-label.t b/src/apisix/t/bk-traffic-label.t new file mode 100644 index 0000000..bcc8784 --- /dev/null +++ b/src/apisix/t/bk-traffic-label.t @@ -0,0 +1,369 @@ +# + +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions and +# limitations under the License. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# + +BEGIN { + if ($ENV{TEST_NGINX_CHECK_LEAK}) { + $SkipReason = "unavailable for the hup tests"; + + } else { + $ENV{TEST_NGINX_USE_HUP} = 1; + undef $ENV{TEST_NGINX_USE_STAP}; + } +} + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.bk-traffic-label") + local ok, err = plugin.check_schema({ + rules = { + { + match = {{"arg_env", "==", "prod"}}, + actions = { + {set_headers = {["X-Test-Header"] = "test"}} + } + } + } + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done + +=== TEST 2: match hit set_headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "bk-proxy-rewrite": { + "uri": "/uri/plugin_proxy_rewrite" + }, + "bk-traffic-label": { + "rules": [ + { + "match": [ + ["arg_env", "==", "prod"] + ], + "actions": [ + {"set_headers": {"X-Test-Header": "test"}} + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + +=== TEST 3: match hit set_headers +--- request +GET /hello?env=prod +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 +x-test-header: test + + +=== TEST 4: match miss do nothing +--- request +GET /hello?env=dev +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 + +=== TEST 5: multiple-actions with weight +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "bk-proxy-rewrite": { + "uri": "/uri/plugin_proxy_rewrite" + }, + "bk-traffic-label": { + "rules": [ + { + "match": [ + ["arg_env", "==", "prod"] + ], + "actions": [ + {"set_headers": {"X-Test-Header": "test"}, "weight": 1}, + {"set_headers": {"X-Test-Header": "test"}, "weight": 1} + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + +=== TEST 6: match hit set_headers +--- request +GET /hello?env=prod +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 +x-test-header: test + +=== TEST 7: only the action with non-zero weight is applied +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "bk-proxy-rewrite": { + "uri": "/uri/plugin_proxy_rewrite" + }, + "bk-traffic-label": { + "rules": [ + { + "match": [ + ["arg_env", "==", "prod"] + ], + "actions": [ + {"set_headers": {"X-Test-Header": "test1"}, "weight": 0}, + {"set_headers": {"X-Test-Header": "test2"}, "weight": 1} + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + +=== TEST 8: match hit set_headers +--- request +GET /hello?env=prod +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 +x-test-header: test2 + +=== TEST 9: only the action with non-zero weight is applied, but do nothing +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "bk-proxy-rewrite": { + "uri": "/uri/plugin_proxy_rewrite" + }, + "bk-traffic-label": { + "rules": [ + { + "match": [ + ["arg_env", "==", "prod"] + ], + "actions": [ + {"set_headers": {"X-Test-Header-1": "test1"}, "weight": 0}, + {"weight": 1} + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + +=== TEST 10: match hit set_headers +--- request +GET /hello?env=prod +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 + +=== TEST 11: multiple matches, all hit +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "bk-proxy-rewrite": { + "uri": "/uri/plugin_proxy_rewrite" + }, + "bk-traffic-label": { + "rules": [ + { + "match": [ + ["arg_env", "==", "prod"] + ], + "actions": [ + {"set_headers": {"X-Test-Header-1": "test1"}} + ] + }, + { + "match": [ + ["arg_type", "==", "foo"] + ], + "actions": [ + {"set_headers": {"X-Test-Header-2": "test2"}} + ] + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + +=== TEST 12: multiple matches, only hit one +--- request +GET /hello?env=dev&type=foo +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 +x-test-header-2: test2 + +=== TEST 13: multiple matches, hit both +--- request +GET /hello?env=prod&type=foo +--- response_body +uri: /uri/plugin_proxy_rewrite +host: localhost +x-real-ip: 127.0.0.1 +x-test-header-1: test1 +x-test-header-2: test2 diff --git a/src/apisix/tests/test-bk-traffic-label.lua b/src/apisix/tests/test-bk-traffic-label.lua new file mode 100644 index 0000000..c93e52b --- /dev/null +++ b/src/apisix/tests/test-bk-traffic-label.lua @@ -0,0 +1,328 @@ +local plugin = require("apisix.plugins.bk-traffic-label") + +describe( + "bk-traffic-label", function() + + local ctx + local conf + + before_each( + function() + ctx = { + var = { + uri = "/foo", + host = "example.com", + remote_addr = "127.0.0.1" + }, + headers = {} + } + end + ) + + context( + "schema validation", function() + it( + "invalid schema: empty", function() + conf = {} + local ok, err = plugin.check_schema(conf) + assert.is_false(ok) + assert.is_not_nil(err) + end + ) + + it( + "valid schema", function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header"] = "test" + } + } + } + } + } + } + local ok, err = plugin.check_schema(conf) + assert.is_true(ok) + assert.is_nil(err) + end + ) + + it( + "valid schema with 1 match and 3 actions with same weight", function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header-1"] = "test1" + }, + weight = 1 + }, + { + set_headers = { + ["X-Test-Header-2"] = "test2" + }, + weight = 1 + }, + { + set_headers = { + ["X-Test-Header-3"] = "test3" + }, + weight = 1 + } + } + } + } + } + local ok, err = plugin.check_schema(conf) + assert.is_true(ok) + assert.is_nil(err) + assert.is_equal(conf.rules[1].actions[1].weight, 1) + assert.is_equal(conf.rules[1].actions[2].weight, 1) + assert.is_equal(conf.rules[1].actions[3].weight, 1) + assert.is_equal(conf.rules[1].total_weight, 3) + end + ) + end + ) + + context( + "1 rules: 1 match 1 action", function() + before_each( + function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header"] = "test" + } + } + } + } + } + } + end + ) + + it( + "match hit set_headers", function() + plugin.check_schema(conf) + + plugin.access(conf, ctx) + assert.is_equal(ctx.headers["X-Test-Header"], "test") + end + ) + + it( + "match miss do nothing", function() + plugin.check_schema(conf) + + ctx.var.uri = "/bar" + plugin.access(conf, ctx) + assert.is_nil(ctx.headers["X-Test-Header"]) + end + ) + end + ) + + context( + "1 rules: 1 match 2 actions, with weight", function() + before_each( + function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header-1"] = "test1" + }, + weight = 1 + }, + { + set_headers = { + ["X-Test-Header-2"] = "test2" + }, + weight = 1 + } + } + } + } + } + end + ) + + it( + "multiple-actions with weight", function() + plugin.check_schema(conf) + + math.randomseed(os.time()) + plugin.access(conf, ctx) + assert.is_true(ctx.headers["X-Test-Header-1"] == "test1" or ctx.headers["X-Test-Header-2"] == "test2") + end + ) + end + ) + + context( + "1 rules: 1 match 2 actions, one with weight 0", function() + before_each( + function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header-1"] = "test1" + }, + weight = 0 + }, + { + set_headers = { + ["X-Test-Header-2"] = "test2" + }, + weight = 1 + } + } + } + } + } + end + ) + + it( + "only the action with non-zero weight is applied", function() + plugin.check_schema(conf) + + plugin.access(conf, ctx) + assert.is_nil(ctx.headers["X-Test-Header-1"]) + assert.is_equal(ctx.headers["X-Test-Header-2"], "test2") + end + ) + end + ) + + context( + "1 rules: 1 match 2 actions, one with weight 0, another weight no set_headers", function() + before_each( + function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header-1"] = "test1" + }, + weight = 0 + }, + { + weight = 1 + } + } + } + } + } + end + ) + + it( + "only the action with non-zero weight is applied, but do nothing", function() + plugin.check_schema(conf) + + plugin.access(conf, ctx) + assert.is_nil(ctx.headers["X-Test-Header-1"]) + assert.is_nil(ctx.headers["X-Test-Header-2"]) + -- assert.is_equal(ctx.headers["X-Test-Header-2"], "test2") + end + ) + end + ) + + context( + "2 rules", function() + + before_each( + function() + conf = { + rules = { + { + match = { + {"uri", "==", "/foo"} + }, + actions = { + { + set_headers = { + ["X-Test-Header-1"] = "test1" + } + } + } + }, + { + match = { + {"host", "==", "example.com"} + }, + actions = { + { + set_headers = { + ["X-Test-Header-2"] = "test2" + } + } + } + } + } + } + end + ) + + it( + "multiple matches, all hit", function() + plugin.check_schema(conf) + + plugin.access(conf, ctx) + assert.is_equal(ctx.headers["X-Test-Header-1"], "test1") + assert.is_equal(ctx.headers["X-Test-Header-2"], "test2") + end + ) + + it( + "multiple matches, only hit one", function() + plugin.check_schema(conf) + + ctx.var.uri = "/bar" + plugin.access(conf, ctx) + assert.is_nil(ctx.headers["X-Test-Header-1"]) + assert.is_equal(ctx.headers["X-Test-Header-2"], "test2") + end + ) + end + ) + + end +)