This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 753914238 feat: add traffic-label plugin (#13342)
753914238 is described below
commit 753914238a8e43797a5362fc2e91c302c6a62a4f
Author: AlinsRan <[email protected]>
AuthorDate: Tue May 12 11:49:51 2026 +0800
feat: add traffic-label plugin (#13342)
---
apisix/cli/config.lua | 1 +
apisix/plugins/traffic-label.lua | 222 +++++++++++
conf/config.yaml.example | 1 +
docs/en/latest/config.json | 1 +
docs/en/latest/plugins/traffic-label.md | 158 ++++++++
docs/zh/latest/config.json | 1 +
docs/zh/latest/plugins/traffic-label.md | 158 ++++++++
t/admin/plugins.t | 1 +
t/plugin/traffic-label.t | 643 ++++++++++++++++++++++++++++++++
t/plugin/traffic-label2.t | 553 +++++++++++++++++++++++++++
10 files changed, 1739 insertions(+)
diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index 2af80eb35..f6daf6e9d 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -247,6 +247,7 @@ local _M = {
"gzip",
-- deprecated and will be removed in a future release
-- "server-info",
+ "traffic-label",
"traffic-split",
"redirect",
"response-rewrite",
diff --git a/apisix/plugins/traffic-label.lua b/apisix/plugins/traffic-label.lua
new file mode 100644
index 000000000..65eb71e53
--- /dev/null
+++ b/apisix/plugins/traffic-label.lua
@@ -0,0 +1,222 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- 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.
+--
+
+local core = require("apisix.core")
+local proxy_rewrite = require("apisix.plugins.proxy-rewrite")
+local expr = require("resty.expr.v1")
+local roundrobin = require("resty.roundrobin")
+local ipairs = ipairs
+local pairs = pairs
+
+local lrucache = core.lrucache.new({
+ ttl = 0, count = 512
+})
+
+
+local schema = {
+ type = "object",
+ properties = {
+ rules = {
+ type = "array",
+ items = {
+ type = "object",
+ properties = {
+ match = {
+ type = "array",
+ items = {
+ anyOf = {
+ {
+ type = "array",
+ },
+ {
+ type = "string",
+ },
+ }
+ },
+ minItems = 1,
+ },
+ actions = {
+ type = "array",
+ items = {
+ type = "object",
+ properties = {
+ set_headers = {
+ description = "new headers for request",
+ type = "object",
+ minProperties = 1,
+ },
+ weight = {
+ type = "integer",
+ default = 1,
+ minimum = 1,
+ },
+ },
+ },
+ minItems = 1,
+ },
+ },
+ required = {"actions"}
+ },
+ minItems = 1,
+ }
+ },
+ required = {"rules"}
+}
+
+local plugin_name = "traffic-label"
+
+local _M = {
+ version = 0.1,
+ -- priority: fault-injection proxy-mirror *-auth > traffic-label >
traffic-split
+ priority = 967,
+ name = plugin_name,
+ schema = schema
+}
+
+
+local function check_set_headers_schema(conf)
+ local header_conf = {
+ headers = conf
+ }
+
+ return proxy_rewrite.check_schema(header_conf)
+end
+
+
+local function set_req_headers(header_conf, ctx)
+ local conf = {
+ headers = header_conf
+ }
+
+ -- reuse proxy-rewrite plugin's logic
+ if conf.headers then
+ if not conf.headers_arr then
+ conf.headers_arr = {}
+
+ for field, value in pairs(conf.headers) do
+ core.table.insert_tail(conf.headers_arr, field, value)
+ end
+ end
+
+ local field_cnt = #conf.headers_arr
+ for i = 1, field_cnt, 2 do
+ core.request.set_header(ctx, conf.headers_arr[i],
+
core.utils.resolve_var(conf.headers_arr[i+1], ctx.var))
+ end
+ end
+end
+
+
+local support_action = {
+ ["set_headers"] = {
+ check_schema = check_set_headers_schema,
+ handle = set_req_headers
+ }
+}
+
+
+function _M.check_schema(conf)
+ local ok, err = core.schema.check(schema, conf)
+ if not ok then
+ return false, err
+ end
+
+ for _, rule in ipairs(conf.rules) do
+ local ok, err = expr.new(rule.match or {})
+ if not ok then
+ return false, "failed to validate the 'match' expression: " ..
+ core.json.encode(rule.match) .. " err: " .. err
+ end
+
+ for _, action in ipairs(rule.actions) do
+ for name, conf in pairs(action) do
+ if name == "weight" then
+ goto CONTINUE
+ end
+
+ local item = support_action[name]
+ if not item then
+ return false, "not supported action: " .. name
+ end
+
+ local ok, err = support_action[name].check_schema(conf)
+ if not ok then
+ return false, "failed to validate the '" .. name .. "'
action: " .. err
+ end
+
+ ::CONTINUE::
+ end
+ end
+ end
+
+ return true
+end
+
+
+local function new_rr_obj(actions)
+ local id_weight_map = {}
+ for i, action in ipairs(actions) do
+ id_weight_map[i] = action.weight
+ end
+
+ return roundrobin:new(id_weight_map)
+end
+
+
+local function next_action(actions)
+ local rr_up, err = lrucache(actions, nil, new_rr_obj, actions)
+ if not rr_up then
+ core.log.error("lrucache roundrobin failed: ", err)
+ return false
+ end
+
+ local id = rr_up:find()
+ return actions[id]
+end
+
+
+function _M.access(conf, ctx)
+ local match_result
+
+ if not conf.rules_arr then
+ conf.rules_arr = {}
+
+ for _, rule in ipairs(conf.rules) do
+ -- if no rule.match, use {} to match all request
+ local expr, _ = expr.new(rule.match or {})
+ core.table.insert_tail(conf.rules_arr, expr)
+ end
+ end
+
+ for i, rule in ipairs(conf.rules) do
+ local expr = conf.rules_arr[i]
+ match_result = expr:eval(ctx.var)
+
+ if match_result then
+ local action = next_action(rule.actions)
+ -- only one action is currently supported
+ for name, conf in pairs(action) do
+ if name ~= "weight" then
+ return support_action[name].handle(conf, ctx)
+ end
+ end
+ end
+ end
+end
+
+
+return _M
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index 0e068d542..2e900bb18 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -530,6 +530,7 @@ plugins: # plugin list (sorted by
priority)
#- brotli # priority: 996
- gzip # priority: 995
#- server-info # priority: 990
+ - traffic-label # priority: 967
- traffic-split # priority: 966
- redirect # priority: 900
- response-rewrite # priority: 899
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index e5459e5bd..836e607d2 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -166,6 +166,7 @@
"plugins/proxy-mirror",
"plugins/api-breaker",
"plugins/traffic-split",
+ "plugins/traffic-label",
"plugins/request-id",
"plugins/proxy-control",
"plugins/client-control",
diff --git a/docs/en/latest/plugins/traffic-label.md
b/docs/en/latest/plugins/traffic-label.md
new file mode 100644
index 000000000..32e8bbaa2
--- /dev/null
+++ b/docs/en/latest/plugins/traffic-label.md
@@ -0,0 +1,158 @@
+---
+title: traffic-label
+keywords:
+ - Apache APISIX
+ - API Gateway
+ - Plugin
+ - traffic-label
+ - traffic tagging
+ - canary release
+description: The traffic-label Plugin sets request headers based on
configurable matching rules with weighted distribution, enabling traffic
tagging and canary deployments.
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/traffic-label" />
+</head>
+
+## Description
+
+The `traffic-label` Plugin sets request headers based on configurable matching
rules. Similar to the [workflow](./workflow.md) Plugin, it evaluates rules in
order and executes an action on the first match. The key difference is that
`traffic-label` supports **weighted distribution** within each rule's action
list, enabling proportional traffic labeling for canary deployments and A/B
testing.
+
+Each rule consists of:
+
+- **`match`** — An optional list of conditions evaluated using
[lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). If
omitted, the rule matches all requests.
+- **`actions`** — An array of actions to execute when the rule matches. Each
action can set request headers and has an optional weight. Traffic is
distributed proportionally across actions using weighted round-robin.
+
+Rules are evaluated in array order. Evaluation stops at the first matching
rule.
+
+## Attributes
+
+| Name | Type | Required | Default | Valid values | Description |
+|------|------|----------|---------|--------------|-------------|
+| rules | array[object] | True | | | List of matching rules. Rules are
evaluated in order; the first match wins. |
+| rules[].match | array | False | `[]` | | Match conditions using
[lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) syntax.
Each element is either an expression array `[var, operator, value]` or the
string `"OR"` / `"AND"` to control logical grouping. When omitted, the rule
matches all requests. |
+| rules[].actions | array[object] | True | | | Actions to execute when the
rule matches. Traffic is distributed across actions based on their `weight`. |
+| rules[].actions[].set_headers | object | False | | | Request headers to set.
Overwrites an existing header or adds a new one. Values support NGINX variables
such as `$remote_addr`. Format: `{"header-name": "value"}`. |
+| rules[].actions[].weight | integer | False | 1 | ≥ 1 | Relative weight for
this action. Traffic proportion = `this weight / sum of all weights in the
rule`. An action with only `weight` set passes traffic through without
modification. |
+
+:::note
+
+- Rules are evaluated in order. Only the first matching rule executes;
subsequent rules are skipped.
+- Currently, `set_headers` is the only supported action type.
+
+:::
+
+## Examples
+
+The examples below demonstrate how you can configure `traffic-label` in
different scenarios.
+
+:::note
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### Label Traffic Based on Request Conditions
+
+The following example demonstrates how to set a request header `X-Server-Id`
to different values based on the `?version` query parameter.
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "traffic-label-route",
+ "uri": "/anything",
+ "plugins": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [["arg_version", "==", "v1"]],
+ "actions": [{"set_headers": {"X-Server-Id": "100"}}]
+ },
+ {
+ "match": [["arg_version", "==", "v2"]],
+ "actions": [{"set_headers": {"X-Server-Id": "200"}}]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+Send a request with `?version=v1`:
+
+```shell
+curl "http://127.0.0.1:9080/anything?version=v1"
+```
+
+The upstream will receive `X-Server-Id: 100`. Send a request with
`?version=v2` and the upstream receives `X-Server-Id: 200`. Requests without a
`version` parameter match no rule and pass through without modification.
+
+### Distribute Traffic Across Actions by Weight
+
+The following example demonstrates weighted distribution using
`traffic-label`. When a request matches the rule, traffic is proportionally
distributed across actions based on their `weight`:
+
+- 30% of requests: `X-Server-Id: 100`
+- 20% of requests: `X-API-Version: v2`
+- 50% of requests: pass through without modification
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "traffic-label-route",
+ "uri": "/anything",
+ "plugins": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [["uri", "==", "/anything"]],
+ "actions": [
+ {
+ "set_headers": {"X-Server-Id": "100"},
+ "weight": 3
+ },
+ {
+ "set_headers": {"X-API-Version": "v2"},
+ "weight": 2
+ },
+ {
+ "weight": 5
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+The total weight is `3 + 2 + 5 = 10`. Across 10 requests, approximately 3 will
have `X-Server-Id: 100`, 2 will have `X-API-Version: v2`, and 5 will pass
through without any added header.
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 947339973..1a66321fa 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -155,6 +155,7 @@
"plugins/proxy-mirror",
"plugins/api-breaker",
"plugins/traffic-split",
+ "plugins/traffic-label",
"plugins/request-id",
"plugins/proxy-control",
"plugins/client-control",
diff --git a/docs/zh/latest/plugins/traffic-label.md
b/docs/zh/latest/plugins/traffic-label.md
new file mode 100644
index 000000000..4180db92e
--- /dev/null
+++ b/docs/zh/latest/plugins/traffic-label.md
@@ -0,0 +1,158 @@
+---
+title: traffic-label
+keywords:
+ - Apache APISIX
+ - API 网关
+ - Plugin
+ - traffic-label
+ - 流量染色
+ - 灰度发布
+description: traffic-label 插件根据可配置的匹配规则和权重分发设置请求头,实现流量染色与灰度发布。
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/traffic-label" />
+</head>
+
+## 描述
+
+`traffic-label` 插件根据可配置的匹配规则为请求设置头部信息。与 [workflow](./workflow.md)
插件类似,该插件按顺序对规则进行求值,并对第一个匹配的规则执行动作。两者的关键区别在于:`traffic-label`
支持在每条规则的动作列表中设置**权重分发**,从而实现按比例的流量染色,适用于灰度发布和 A/B 测试场景。
+
+每条规则由以下两部分组成:
+
+- **`match`** — 使用
[lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)
定义的可选匹配条件。省略时该规则匹配所有请求。
+- **`actions`** — 规则命中时要执行的动作数组。每个动作可设置请求头,并带有可选的权重。流量按权重使用加权轮询算法分发到各动作。
+
+规则按数组顺序逐条求值,命中第一条后停止。
+
+## 属性
+
+| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
+|------|------|--------|--------|--------|------|
+| rules | array[object] | 是 | | | 匹配规则列表,按数组顺序求值,第一个命中的规则生效。 |
+| rules[].match | array | 否 | `[]` | | 使用
[lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)
语法定义的匹配条件。每个元素为表达式数组 `[变量, 运算符, 值]`,或字符串 `"OR"` / `"AND"` 用于控制逻辑分组。省略时匹配所有请求。 |
+| rules[].actions | array[object] | 是 | | | 规则命中时要执行的动作数组。流量按各动作的 `weight`
进行分发。 |
+| rules[].actions[].set_headers | object | 否 | | |
要设置的请求头。已存在同名头部时覆盖,不存在时新增。值支持 NGINX 变量,如 `$remote_addr`。格式:`{"头部名称": "值"}`。 |
+| rules[].actions[].weight | integer | 否 | 1 | ≥ 1 | 该动作的相对权重。流量占比 = 该动作权重 /
规则中所有动作权重之和。仅设置 `weight` 而不配置其他动作,表示该比例的流量不做任何修改直接通过。 |
+
+:::note
+
+- 规则按顺序求值,仅执行第一个命中的规则,后续规则不再匹配。
+- 目前 `set_headers` 是唯一支持的动作类型。
+
+:::
+
+## 示例
+
+以下示例演示了如何在不同场景中使用 `traffic-label` 插件。
+
+:::note
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### 按请求条件为流量打标签
+
+以下示例演示如何根据 `?version` 查询参数,将请求头 `X-Server-Id` 设置为不同的值。
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "traffic-label-route",
+ "uri": "/anything",
+ "plugins": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [["arg_version", "==", "v1"]],
+ "actions": [{"set_headers": {"X-Server-Id": "100"}}]
+ },
+ {
+ "match": [["arg_version", "==", "v2"]],
+ "actions": [{"set_headers": {"X-Server-Id": "200"}}]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+发送带 `?version=v1` 的请求:
+
+```shell
+curl "http://127.0.0.1:9080/anything?version=v1"
+```
+
+上游服务将收到 `X-Server-Id: 100`。发送 `?version=v2` 的请求时,上游服务将收到 `X-Server-Id: 200`。不带
`version` 参数的请求不命中任何规则,将直接透传。
+
+### 按权重将流量分发到不同动作
+
+以下示例演示使用 `traffic-label` 进行加权分发。当请求命中规则时,流量按动作的 `weight` 按比例分配:
+
+- 30% 的请求:设置 `X-Server-Id: 100`
+- 20% 的请求:设置 `X-API-Version: v2`
+- 50% 的请求:直接通过,不做任何修改
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "traffic-label-route",
+ "uri": "/anything",
+ "plugins": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [["uri", "==", "/anything"]],
+ "actions": [
+ {
+ "set_headers": {"X-Server-Id": "100"},
+ "weight": 3
+ },
+ {
+ "set_headers": {"X-API-Version": "v2"},
+ "weight": 2
+ },
+ {
+ "weight": 5
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+权重总和为 `3 + 2 + 5 = 10`。每 10 个请求中,约有 3 个将带有 `X-Server-Id: 100`,2 个将带有
`X-API-Version: v2`,5 个将不带任何新增请求头直接通过。
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index f45256f17..b9f3846ca 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -112,6 +112,7 @@ limit-conn
limit-count
limit-req
gzip
+traffic-label
traffic-split
redirect
response-rewrite
diff --git a/t/plugin/traffic-label.t b/t/plugin/traffic-label.t
new file mode 100644
index 000000000..23e289a48
--- /dev/null
+++ b/t/plugin/traffic-label.t
@@ -0,0 +1,643 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $extra_yaml_config = <<_EOC_;
+plugins:
+ - traffic-label
+ - proxy-rewrite
+_EOC_
+
+ if (!$block->extra_yaml_config) {
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+ }
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+ $block->set_value("no_error_log", "[error]");
+ }
+});
+
+run_tests();
+
+
+__DATA__
+
+=== TEST 1: Use unsupported action
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"]
+ ],
+ "actions":[
+ {
+ "add_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- error_code: 400
+--- response_body eval
+qr/not supported action: add_headers/
+
+
+
+=== TEST 2: Only one operator are supported in the same level
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ "AND",
+ "OR",
+ ["uri", "==", "/echo"],
+ ["arg_foo", "==", "bar"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- error_code: 500
+--- error_log eval
+qr/bad argument/
+
+
+
+=== TEST 3: use traffic-label plugin to override one req header
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["arg_foo", "==", "bar"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: trigger traffic-label, mismatch
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 5: trigger traffic-label
+--- request
+GET /echo?foo=bar
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 6: use traffic-label plugin to add a new req header
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100,
+ "resp-X-content-type":
"json"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 7: trigger traffic-label
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+X-content-type: json
+
+
+
+=== TEST 8: AND condition in match
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"],
+ ["arg_foo", "==", "bar"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 9: mismatch the condition
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 10: match the condition
+--- request
+GET /echo?foo=bar
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 11: OR condition in match
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ "OR",
+ ["arg_foo", "==", "bar"],
+ ["uri", "==", "/echo"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 12: match the condition
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 13: wrong weight in rules
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ },
+ "weight": 0.2
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- error_code: 400
+--- response_body eval
+qr/property \\"weight\\" validation failed/
+
+
+
+=== TEST 14: ipmatch operator, mismatch
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["remote_addr", "ipmatch",
"127.0.0.2"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ },
+ "weight": 1
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 15: trigger traffic-label
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 16: ipmatch operator, match
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["remote_addr", "ipmatch",
"127.0.0.1"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ },
+ "weight": 1
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 17: trigger traffic-label
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 18: nested expr
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ "AND",
+ ["remote_addr", "ipmatch",
"127.0.0.1"],
+ [
+ "AND",
+ ["uri", "==", "/echo"],
+ ["arg_foo", "==", "bar"]
+ ]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ },
+ "weight": 1
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 19: trigger traffic-label, mismatch
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 20: trigger traffic-label, match
+--- request
+GET /echo?foo=bar
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
diff --git a/t/plugin/traffic-label2.t b/t/plugin/traffic-label2.t
new file mode 100644
index 000000000..70271f20d
--- /dev/null
+++ b/t/plugin/traffic-label2.t
@@ -0,0 +1,553 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $extra_yaml_config = <<_EOC_;
+plugins:
+ - traffic-label
+ - proxy-rewrite
+_EOC_
+
+ if (!$block->extra_yaml_config) {
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+ }
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+ $block->set_value("no_error_log", "[error]");
+ }
+});
+
+run_tests();
+
+
+__DATA__
+
+=== TEST 1: use traffic-label plugin with proxy-rewrite plugin
+--- 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": {
+ "proxy-rewrite": {
+ "uri": "/echo"
+ },
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["arg_foo", "==", "bar"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/*"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 2: trigger workflow
+--- request
+GET /echo_not_exist
+--- more_headers
+X-server-id: 100
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 3: trigger workflow
+--- request
+GET /echo_not_exist?foo=bar
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 4: trigger workflow
+--- request
+GET /echo?foo=bar
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 5: trigger workflow
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 6: If there is no match condition in rule, all requests are matched
by default
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 7: match the condition
+--- request
+GET /echo
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 100
+
+
+
+=== TEST 8: multiple 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100,
+ "X-request-id": "id2"
+ },
+ "weight": 1
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 9: trigger traffic-label
+--- pipelined_requests eval
+[
+ "GET /echo",
+ "GET /echo",
+]
+--- more_headers
+X-server-id: 200
+X-request-id: id1
+--- response_headers eval
+[
+ "X-Server-id: 100",
+ "X-request-id: id2"
+]
+
+
+
+=== TEST 10: multiple actions
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 100
+ },
+ "weight": 1
+ },
+ {
+ "set_headers": {
+ "X-server-id": 200
+ },
+ "weight": 1
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 11: trigger traffic-label
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/echo"
+
+ local resp_arr = {}
+ for i = 1, 2 do
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, {method = "GET"})
+ if not res then
+ ngx.say(err)
+ return
+ end
+ table.insert(resp_arr, res.headers["X-Server-id"])
+ end
+
+ table.sort(resp_arr, cmd)
+
+ ngx.say(require("toolkit.json").encode(resp_arr))
+ ngx.exit(200)
+ }
+ }
+--- request
+GET /t
+--- response_body
+["100","200"]
+
+
+
+=== TEST 12: no action
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["uri", "==", "/echo"]
+ ],
+ "actions": [
+ {
+ "weight": 1
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 13: match the condition
+--- request
+GET /echo?foo=bar
+--- more_headers
+X-server-id: 200
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 14: multiple rules
+--- 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": {
+ "traffic-label": {
+ "rules": [
+ {
+ "match": [
+ ["arg_foo", "==", "water"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 200
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ ["arg_foo", "==", "bar"]
+ ],
+ "actions": [
+ {
+ "set_headers": {
+ "X-server-id": 300
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 15: match one condition
+--- request
+GET /echo?foo=water
+--- more_headers
+X-server-id: 100
+--- response_headers
+X-Server-id: 200
+
+
+
+=== TEST 16: match one condition
+--- request
+GET /echo?foo=bar
+--- more_headers
+X-server-id: 100
+--- response_headers
+X-Server-id: 300
+
+
+
+=== TEST 17: set plugin without configuration
+--- 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": {
+ "traffic-label": {
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- error_code: 400
+--- response_body eval
+qr/property \\"rules\\" is required/
+
+
+
+=== TEST 18: set plugin with empty rules
+--- 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": {
+ "traffic-label": {
+ "rules": []
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/echo"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- error_code: 400
+--- response_body eval
+qr/expect array to have at least 1 items/