Add traffic tag plugin (#1296)

This commit is contained in:
澄潭
2024-09-11 11:44:37 +08:00
committed by GitHub
parent f471c0a99f
commit a4e696b957
12 changed files with 1019 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
# traffic-tag插件说明文档
本文档提供了 `traffic-tag` 插件的配置选项的详细信息。此配置用于管理基于灰度发布和测试目的定义的特定规则的流量标记。
## 功能说明
`traffic-tag` 插件允许根据权重或特定请求内容通过添加特定请求头的方式对请求流量进行标记。它支持复杂的逻辑来确定如何根据用户定义的标准标记流量。
## 配置字段
此部分提供了配置字段的详细描述。
| 字段名称 | 类型 | 默认值 | 是否必填 | 描述 |
|----------------|----------|-------|---------|-------------------------------------------------------------|
| `conditionGroups` | array of object | - | 否 | 定义基于内容的标记条件组,详细结构见**条件组配置**。 |
| `weightGroups` | array of object | - | 否 | 定义基于权重的标记条件组,详细结构见**权重组配置**。 |
| `defaultTagKey` | string | - | 否 | 默认的标记键名,当未匹配到任何条件时使用。当且仅当同时配置了**defaultTagVal**时生效 |
| `defaultTagVal` | string | - | 否 | 默认的标记值,当未匹配到任何条件时使用。当且仅当同时配置了**defaultTagKey**时生效 |
### 条件组配置
`conditionGroups` 中每一项的配置字段说明如下:
| 字段名称 | 类型 | 默认值 | 是否必填 | 描述 |
|--------------|--------|-------|---------|-------------------------------------------------------------|
| `headerName` | string | - | 是 | 要添加或修改的 HTTP 头名称。 |
| `headerValue`| string | - | 是 | HTTP 头的值。 |
| `logic` | string | - | 是 | 条件组中的逻辑关系,支持 `and``or`,必须为小写字母。 |
| `conditions` | array of object | - | 是 | 描述具体的标记条件,详细结构如下。 |
---
`conditions` 中每一项的配置字段说明如下:
| 字段名称 | 类型 | 默认值 | 是否必填 | 描述 |
|----------------|--------|-------|---------|-------------------------------------------------------------|
| `conditionType`| string | - | 是 | 条件类型,支持 `header``parameter``cookie`。 |
| `key` | string | - | 是 | 条件的关键字。 |
| `operator` | string | - | 是 | 操作符,支持 `equal``not_equal``prefix``in``not_in``regex``percentage`。 |
| `value` | array of string | - | 是 | 条件的值,**仅当**操作符为 `in``not_in` 时支持配置多个值。 |
> **说明:当 `operator` 为 `regex` 时,使用的正则表达式引擎是 [RE2](https://github.com/google/re2)。详情请参阅 [RE2 官方文档](https://github.com/google/re2/wiki/Syntax)。
### 权重组配置
`weightGroups` 中每一项的配置字段说明如下:
| 字段名称 | 类型 | 默认值 | 是否必填 | 描述 |
|--------------|----------|-------|---------|-------------------------------------------------------------|
| `headerName` | string | - | 是 | 要添加或修改的 HTTP 头名称。 |
| `headerValue`| string | - | 是 | HTTP 头的值。 |
| `weight` | integer | - | 是 | 流量权重百分比。 |
### 操作符说明
| 操作符 | 描述 |
|-------------|------------------------------------------|
| `equal` | 精确匹配,值需要完全相等 |
| `not_equal` | 不等匹配,值不相等时满足条件 |
| `prefix` | 前缀匹配,指定值是实际值的前缀时满足条件 |
| `in` | 包含匹配,实际值需要在指定的列表中 |
| `not_in` | 排除匹配,实际值不在指定的列表中时满足条件|
| `regex` | 正则表达式匹配,按照正则表达式规则匹配 |
| `percentage`| 百分比匹配,原理:`hash(get(key)) % 100 < value` 成立时满足条件|
> **提示:关于`percentage`和`weight`的区别**
>
> - **`percentage`操作符**:用于条件表达式中,基于指定的百分比和指定的键值对来判断是否执行某个操作。对于一个相同的键值对,多次匹配的结果是幂等的,即这一次命中条件,下一次也会命中。
> - **`weight`字段**:用于定义不同处理路径的流量权重。在基于权重的流量标记中,`weight`确定了某个路径应接收的流量比例。与`percentage`不同的是,由于没有指定固定的对比依据而是基于随机权重分布,同一个请求的多次匹配可能匹配多个结果。
>
> 使用`percentage`进行条件匹配时,判断每个请求是否满足特定百分比条件;而`weight`则是静态随机分配整体流量的比例。
## 配置示例
**例1: 基于内容的匹配**
按照下例的配置,满足请求头`role` 的值是`user``viwer``editor`其中之一且存在查询参数`foo=bar`的请求将被添加请求头`x-mse-tag: gray`。由于配置了`defaultTagKey``defaultTagVal`,当未匹配到任何条件时,请求将被添加请求头`x-mse-tag: base`
```yaml
defaultTagKey: x-mse-tag
defaultTagVal: base
conditionGroups:
- headerName: x-mse-tag
headerValue: gray
logic: and
conditions:
- conditionType: header
key: role
operator: in
value:
- user
- viewer
- editor
- conditionType: parameter
key: foo
operator: equal
value:
- bar
```
**例子2: 基于权重的匹配**
按照下列配置请求将有30%几率被添加请求头`x-mse-tag: gray`30%几率被添加请求头`x-mse-tag: blue`40%几率不添加请求头。
```yaml
# 权重总和为100下例中未配置的40权重将不添加header
weightGroups:
- headerName: x-mse-tag
headerValue: gray
weight: 30
- headerName: x-mse-tag
headerValue: blue
weight: 30
```

View File

@@ -0,0 +1,113 @@
# Traffic-Tag Plugin Documentation
This document provides detailed information about the configuration options for the `traffic-tag` plugin. This configuration is used to manage traffic tags based on rules defined for canary releases and testing purposes.
## Feature Description
The `traffic-tag` plugin allows for tagging request traffic by adding specific request headers based on weight or specific request content. It supports complex logic to determine how to tag traffic based on user-defined criteria.
## Configuration Fields
This section provides a detailed description of the configuration fields.
| Field Name | Type | Default | Required | Description |
|-------------------|------------------|---------|----------|-------------------------------------------------------------------|
| `conditionGroups` | array of objects | - | No | Defines groups of content-based tagging conditions, see **Condition Group Configuration**. |
| `weightGroups` | array of objects | - | No | Defines groups of weight-based tagging conditions, see **Weight Group Configuration**. |
| `defaultTagKey` | string | - | No | The default tag key name used when no conditions are matched. It only takes effect when **defaultTagValue** is also configured. |
| `defaultTagValue` | string | - | No | The default tag value used when no conditions are matched. It only takes effect when **defaultTagKey** is also configured. |
### Condition Group Configuration
Each item in `conditionGroups` has the following configuration fields:
| Field Name | Type | Default | Required | Description |
|--------------|--------|---------|----------|------------------------------------------------------------------|
| `headerName` | string | - | Yes | The name of the HTTP header to add or modify. |
| `headerValue`| string | - | Yes | The value of the HTTP header. |
| `logic` | string | - | Yes | The logical relation within the condition group, supports `and`, `or`, all in lowercase. |
| `conditions` | array of objects | - | Yes | Describes specific tagging conditions, detailed structure below. |
---
The configuration fields for each item in `conditions` are as follows:
| Field Name | Type | Default | Required | Description |
|----------------|--------|---------|----------|---------------------------------------------------------------------|
| `conditionType`| string | - | Yes | The type of condition, supports `header`, `parameter`, `cookie`. |
| `key` | string | - | Yes | The keyword for the condition. |
| `operator` | string | - | Yes | The operator, supports `equal`, `not_equal`, `prefix`, `in`, `not_in`, `regex`, `percentage`. |
| `value` | array of strings | - | Yes | The value for the condition, **only** supports multiple values for `in` and `not_in`. |
> **Note: When `operator` is `regex`, the regex engine used is [RE2](https://github.com/google/re2). For more details, see the [RE2 official documentation](https://github.com/google/re2/wiki/Syntax).**
### Weight Group Configuration
Each item in `weightGroups` has the following configuration fields:
| Field Name | Type | Default | Required | Description |
|--------------|---------|---------|----------|--------------------------------------------------------|
| `headerName` | string | - | Yes | The name of the HTTP header to add or modify. |
| `headerValue`| string | - | Yes | The value of the HTTP header. |
| `weight` | integer | - | Yes | The percentage of traffic weight. |
### Operator Descriptions
| Operator | Description |
|--------------|---------------------------------------------------------|
| `equal` | Exact match, the value must be completely equal. |
| `not_equal` | Not equal match, condition is met when values are not equal. |
| `prefix` | Prefix match, condition is met when the specified value is a prefix of the actual value. |
| `in` | Inclusion match, actual value needs to be in the specified list. |
| `not_in` | Exclusion match, condition is met when the actual value is not in the specified list. |
| `regex` | Regex match, matches according to regex rules. |
| `percentage` | Percentage match, the principle: `hash(get(key)) % 100 < value` satisfies the condition. |
> **Tip: Differences between `percentage` and `weight`**
>
> - **`percentage` operator**: Used in condition expressions, based on a specified percentage and specified key-value pair to determine whether to perform a certain action. For the same key-value pair, the result of multiple matches is idempotent, i.e., if it hits the condition this time, it will also hit next time.
> - **`weight` field**: Used to define the traffic weight of different treatment paths. In weight-based traffic marking, `weight` determines the proportion of traffic that a path should receive. Unlike `percentage`, which is based on a fixed comparison basis, `weight` involves a random distribution of traffic weight, so the same request may match multiple outcomes in multiple matches.
>
> When using `percentage` for condition matching, each request is evaluated to see if it meets a specific percentage condition; whereas `weight` statically randomly allocates the proportion of overall traffic.
## Configuration Example
**Example 1: Content-Based Matching**
According to the following configuration, requests that meet both the condition of the request header `role` being one of `user`, `viewer`, or `editor` and the query parameter `foo=bar` will have the request header `x-mse-tag: gray` added. Since `defaultTagKey` and `defaultTagVal` are configured, when no conditions are matched, the request header `x-mse-tag: base` will be added to the request.
```yaml
defaultTagKey: x-mse-tag
defaultTagVal: base
conditionGroups:
- headerName: x-mse-tag
headerValue: gray
logic: and
conditions:
- conditionType: header
key: role
operator: in
value:
- user
- viewer
- editor
- conditionType: parameter
key: foo
operator: equal
value:
- bar
```
**Example 2: Weight-Based Matching**
According to the following configuration, requests have a 30% chance of having the request header `x-mse-tag: gray` added, a 30% chance of having `x-mse-tag: blue` added, and a 40% chance of not having any header added.
```yaml
# The total weight sums to 100, the unconfigured 40 weight will not add a header
weightGroups:
- headerName: x-mse-tag
headerValue: gray
weight: 30
- headerName: x-mse-tag
headerValue: blue
weight: 30
```

View File

@@ -0,0 +1,50 @@
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: traffic-tag
namespace: higress-system
spec:
defaultConfig:
conditionGroups:
- headerName: x-mse-tag-1
headerValue: gray
logic: or
conditions:
- conditionType: header
key: x-user-type
operator: prefix
value:
- test
- headerName: x-mse-tag-2
headerValue: blue
logic: and
conditions:
- conditionType: header
key: x-type
operator: in
value:
- type1
- type2
- type3
- conditionType: header
key: x-mod
operator: regex
value:
- "^[a-zA-Z0-9]{8}$"
- headerName: x-mse-tag-3
headerValue: green
logic: and
conditions:
- conditionType: header
key: user_id
operator: percentage
value:
- 60
weightGroups:
- headerName: x-higress-canary
headerValue: gray
weight: 30
- headerName: x-higress-canary
headerValue: base
weight: 70
url: file:///opt/plugins/wasm-go/extensions/traffic-tag/plugin.wasm

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,62 @@
apiVersion: 2.0.0
info:
# 插件类型
type: enterprise
# 功能分类
category: traffic
# 插件名称
name: traffic-tag
# 国际版插件标题
title: traffic-tag
x-title-i18n:
# 插件标题
zh-CN: traffic-tag
# 国际版插件简介
description: Mark request traffic by adding specific request headers based on weight or specific request content.
x-description-i18n:
# 中文插件简介
zh-CN: 根据权重或特定请求内容通过添加特定请求头的方式对请求流量进行标记。
# 插件版本
version: 1.0.0
# 插件镜像名称
image: platform_wasm/traffic-tag
# 支持的最小网关版本
gatewayMinVersion: ""
spec:
# 执行阶段
phase: default
# 执行优先级
priority: 400
configSchema:
openAPIV3Schema:
# 配置示例字段
example:
conditionGroups:
- headerName: x-mse-tag
headerValue: gray
logic: and
conditions:
- conditionType: header
key: role
operator: not_in
value:
- user
- viewer
- editor
routeConfigSchema:
openAPIV3Schema:
# 域名/路由级配置示例字段
example:
conditionGroups:
- headerName: x-mse-tag
headerValue: gray
logic: and
conditions:
- conditionType: header
key: role
operator: not_in
value:
- user
- viewer
- editor

View File

@@ -0,0 +1,178 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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.
package main
import (
"crypto/sha256"
"encoding/binary"
"errors"
"strconv"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
)
func onContentRequestHeaders(conditionGroups []ConditionGroup, log wrapper.Log) bool {
if len(conditionGroups) == 0 {
return false
}
for _, cg := range conditionGroups {
if matchCondition(&cg, log) {
addTagHeader(cg.HeaderName, cg.HeaderValue, log)
return true
}
}
return false
}
// matchCondition matches the single condition group
func matchCondition(conditionGroup *ConditionGroup, log wrapper.Log) bool {
for _, condition := range conditionGroup.Conditions {
conditionKeyValue, err := getConditionValue(condition, log)
if err != nil {
log.Debugf("failed to get condition value: %s", err)
if conditionGroup.Logic == "and" {
return false
}
continue
}
switch condition.Operator {
case Op_Equal:
if conditionKeyValue == condition.Value[0] && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s == %s", conditionKeyValue, condition.Value[0])
return true
} else if conditionKeyValue != condition.Value[0] && conditionGroup.Logic == "and" {
return false
}
case Op_NotEqual:
if conditionKeyValue != condition.Value[0] && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s != %s", conditionKeyValue, condition.Value[0])
return true
} else if conditionKeyValue == condition.Value[0] && conditionGroup.Logic == "and" {
return false
}
case Op_Prefix:
if strings.HasPrefix(conditionKeyValue, condition.Value[0]) && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s prefix %s", conditionKeyValue, condition.Value[0])
return true
} else if !strings.HasPrefix(conditionKeyValue, condition.Value[0]) && conditionGroup.Logic == "and" {
return false
}
case Op_Regex:
if _, ok := regexCache[condition.Value[0]]; !ok {
err := compileRegex(condition.Value[0])
if err != nil {
log.Warnf("failed to compile regex: %s", err)
return false
}
}
regex := regexCache[condition.Value[0]]
if regex.MatchString(conditionKeyValue) && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s regex %s", conditionKeyValue, condition.Value[0])
return true
} else if !regex.MatchString(conditionKeyValue) && conditionGroup.Logic == "and" {
log.Debugf("condition not match: %s regex %s", conditionKeyValue, condition.Value[0])
return false
}
case Op_In:
isMatch := false
for _, v := range condition.Value {
if v == conditionKeyValue {
isMatch = true
break
}
}
if isMatch && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s in %v", conditionKeyValue, condition.Value)
return true
} else if !isMatch && conditionGroup.Logic == "and" {
return false
}
case Op_NotIn:
isMatch := false
for _, v := range condition.Value {
if v == conditionKeyValue {
isMatch = true
break
}
}
if !isMatch && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s not in %v", conditionKeyValue, condition.Value)
return true
} else if isMatch && conditionGroup.Logic == "and" {
return false
}
case Op_Percent:
percentThresholdInt, err := strconv.Atoi(condition.Value[0])
if err != nil {
log.Infof("invalid percent threshold config: %s", err)
return false
}
// hash(value) % 100 < percent
hash := sha256.Sum256([]byte(conditionKeyValue))
hashInt64 := int64(binary.BigEndian.Uint64(hash[:8]) % 100)
log.Debugf("hashInt64: %d", hashInt64)
if hashInt64 < int64(percentThresholdInt) && conditionGroup.Logic == "or" {
log.Debugf("condition match: %d < %d", hashInt64, percentThresholdInt)
return true
} else if hashInt64 >= int64(percentThresholdInt) && conditionGroup.Logic == "and" {
log.Debugf("condition not match: %d >= %d", hashInt64, percentThresholdInt)
return false
}
default:
log.Criticalf("invalid operator: %s", condition.Operator)
return false
}
}
return len(conditionGroup.Conditions) > 0 && conditionGroup.Logic == "and" // all conditions are matched
}
func getConditionValue(condition ConditionRule, log wrapper.Log) (string, error) {
// log.Debugf("conditionType: %s, key: %s", condition.ConditionType, condition.Key)
switch condition.ConditionType {
case Type_Header:
// log.Debug("Hit header condition")
log.Debugf("Hit header condition, key: %s", condition.Key)
return proxywasm.GetHttpRequestHeader(condition.Key)
case Type_Cookie:
log.Debugf("Hit cookie condition, key: %s", condition.Key)
requestCookie, err := proxywasm.GetHttpRequestHeader(Type_Cookie)
ckv, found := parseCookie(requestCookie, condition.Key)
if !found {
return "", errors.New("cookie not found")
}
return ckv, err
case Type_Parameter:
log.Debugf("Hit parameter condition, key: %s", condition.Key)
urlStr, err := getFullRequestURL()
if err != nil {
return "", err
}
return getQueryParameter(urlStr, condition.Key)
default:
log.Criticalf("invalid conditionType: %s", condition.ConditionType)
return "", errors.New("invalid conditionType: " + condition.ConditionType)
}
}

View File

@@ -0,0 +1,24 @@
module github.com/alibaba/higress/plugins/wasm-go/extensions/traffic-tag
go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v1.4.1-0.20240612065146-12cc44b3247f
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
github.com/tidwall/gjson v1.17.1
github.com/wasilibs/go-re2 v1.6.0
)
require (
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tetratelabs/wazero v1.7.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/resp v0.1.1 // indirect
golang.org/x/sys v0.21.0 // indirect
)

View File

@@ -0,0 +1,28 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a h1:tdPcGgyiH0K+SbsJBBm2oPyEIOTAvLBwD9TuUwVtZho=
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
github.com/wasilibs/go-re2 v1.6.0 h1:CLlhDebt38wtl/zz4ww+hkXBMcxjrKFvTDXzFW2VOz8=
github.com/wasilibs/go-re2 v1.6.0/go.mod h1:prArCyErsypRBI/jFAFJEbzyHzjABKqkzlidF0SNA04=
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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.
package main
import (
"math/rand"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
const (
PluginName = "traffic-tag"
ConditionGroups = "conditionGroups"
WeightGroups = "weightGroups"
HeaderName = "headerName"
HeaderValue = "headerValue"
Conditions = "conditions"
MatchLogic = "logic"
CondKeyType = "conditionType"
CondKey = "key"
CondMatchType = "operator"
CondValue = "value"
Weight = "weight"
)
const (
DefaultTagKey = "defaultTagKey"
DefaultTagVal = "defaultTagVal"
Type_Content = "content"
Type_Weight = "weight"
Type_Header = "header"
Type_Cookie = "cookie"
Type_Parameter = "parameter"
Op_Prefix = "prefix"
Op_Equal = "equal"
Op_NotEqual = "not_equal"
Op_Regex = "regex"
Op_In = "in"
Op_NotIn = "not_in"
Op_Percent = "percentage"
TotalWeight = 100
)
type TrafficTagConfig struct {
ConditionGroups []ConditionGroup `json:"conditionGroups,omitempty"`
WeightGroups []WeightGroup `json:"weightGroups,omitempty"`
DefaultTagKey string `json:"defaultTagKey,omitempty"`
DefaultTagVal string `json:"defaultTagVal,omitempty"`
randGen *rand.Rand
}
type ConditionGroup struct {
HeaderName string `json:"headerName"`
HeaderValue string `json:"headerValue"`
Logic string `json:"logic"`
Conditions []ConditionRule `json:"conditions"`
}
type ConditionRule struct {
ConditionType string `json:"conditionType"`
Key string `json:"key"`
Operator string `json:"operator"`
Value []string `json:"value"`
}
type WeightGroup struct {
HeaderName string `json:"headerName"`
HeaderValue string `json:"headerValue"`
Weight int64 `json:"weight"`
Accumulate int64
}
func main() {
wrapper.SetCtx(
PluginName,
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
func parseConfig(json gjson.Result, config *TrafficTagConfig, log wrapper.Log) error {
if err := jsonValidate(json, log); err != nil {
return err
}
err := parseContentConfig(json, config, log)
if err != nil {
return err
}
return parseWeightConfig(json, config, log)
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config TrafficTagConfig, log wrapper.Log) types.Action {
if add := (onContentRequestHeaders(config.ConditionGroups, log) || onWeightRequestHeaders(config.WeightGroups, config.randGen, log)); !add {
setDefaultTag(config.DefaultTagKey, config.DefaultTagVal, log)
}
return types.ActionContinue
}

View File

@@ -0,0 +1,204 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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.
package main
import (
"errors"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
regexp "github.com/wasilibs/go-re2"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tidwall/gjson"
)
var regexCache = map[string]*regexp.Regexp{}
func parseContentConfig(json gjson.Result, config *TrafficTagConfig, log wrapper.Log) error {
var parseError error
config.ConditionGroups = []ConditionGroup{}
json.Get(ConditionGroups).ForEach(func(_, group gjson.Result) bool {
groupResults := gjson.GetMany(group.Raw, HeaderName, HeaderValue, MatchLogic, Conditions)
cg := ConditionGroup{
HeaderName: groupResults[0].String(),
HeaderValue: groupResults[1].String(),
Logic: strings.ToLower(groupResults[2].String()),
Conditions: []ConditionRule{},
}
if cg.HeaderName == "" || cg.HeaderValue == "" || cg.Logic == "" || (cg.Logic != "and" && cg.Logic != "or") {
parseError = fmt.Errorf("invalid condition group: %s, HeaderName: %s, HeaderValue: %s, Logic: %s", group.String(), cg.HeaderName, cg.HeaderValue, cg.Logic)
return false
}
groupResults[3].ForEach(func(_, cond gjson.Result) bool {
results := gjson.GetMany(cond.Raw, CondKeyType, CondKey, CondMatchType, CondValue)
c := ConditionRule{
ConditionType: strings.ToLower(results[0].String()),
Key: results[1].String(),
Operator: strings.ToLower(results[2].String()),
Value: extractStringArray(results[3]),
}
parseError = c.validate()
if parseError != nil {
return false
}
// precompile regex
if c.Operator == Op_Regex {
err := compileRegex(c.Value[0])
if err != nil {
parseError = err
return false
}
}
cg.Conditions = append(cg.Conditions, c)
return true
})
config.ConditionGroups = append(config.ConditionGroups, cg)
return true
})
log.Infof("Completed parsing condition config: %v", config.ConditionGroups)
return parseError
}
func parseWeightConfig(json gjson.Result, config *TrafficTagConfig, log wrapper.Log) error {
var parseError error
var accumulatedWeight int64
config.WeightGroups = []WeightGroup{}
config.randGen = rand.New(rand.NewSource(time.Now().UnixNano()))
// parse default tag key and value
if k, v := json.Get(DefaultTagKey), json.Get(DefaultTagVal); k.Exists() && v.Exists() {
config.DefaultTagKey = k.String()
config.DefaultTagVal = v.String()
log.Debugf("Default tag key: %s, value: %s", config.DefaultTagKey, config.DefaultTagVal)
}
json.Get(WeightGroups).ForEach(func(_, header gjson.Result) bool {
results := gjson.GetMany(header.Raw, HeaderName, HeaderValue, Weight)
wh := WeightGroup{
HeaderName: results[0].String(),
HeaderValue: results[1].String(),
Weight: results[2].Int(),
}
if wh.HeaderName == "" || wh.HeaderValue == "" || wh.Weight < 0 || wh.Weight > TotalWeight {
parseError = errors.New("invalid weight config: " + header.String())
return false
}
if accumulatedWeight += wh.Weight; accumulatedWeight > TotalWeight {
parseError = errors.New("total weight exceeds: " + strconv.Itoa(TotalWeight))
return false
}
wh.Accumulate = accumulatedWeight
config.WeightGroups = append(config.WeightGroups, wh)
return true
})
if len(config.WeightGroups) > 0 {
log.Infof("Completed parsing weight config: %v", config.WeightGroups)
} else {
log.Infof("No weight config configured")
}
return parseError
}
func compileRegex(pattern string) error {
if _, exists := regexCache[pattern]; !exists {
compiled, err := regexp.Compile(pattern)
if err != nil {
return err
}
regexCache[pattern] = compiled
proxywasm.LogDebug("compiled regex: " + pattern)
}
return nil
}
func extractStringArray(result gjson.Result) []string {
var values []string
for _, v := range result.Array() {
values = append(values, v.String())
}
return values
}
func (c ConditionRule) String() string {
return fmt.Sprintf("ConditionType: %s, Key: %s, Operator: %s, Value: %v", c.ConditionType, c.Key, c.Operator, c.Value)
}
func (c ConditionRule) validate() error {
if c.ConditionType == "" {
return errors.New("conditionType cannot be empty")
}
if c.Key == "" {
return errors.New("key cannot be empty")
}
if c.Operator == "" {
return errors.New("operator cannot be empty")
}
var validOperators = map[string]bool{
Op_Equal: true,
Op_NotEqual: true,
Op_Prefix: true,
Op_In: true,
Op_NotIn: true,
Op_Regex: true,
Op_Percent: true,
}
if !validOperators[c.Operator] {
return fmt.Errorf("invalid operator: '%s'", c.Operator)
}
if c.ConditionType != Type_Header && c.ConditionType != Type_Parameter && c.ConditionType != Type_Cookie {
return fmt.Errorf("invalid conditionType: '%s'", c.ConditionType)
}
switch c.Operator {
case Op_In, Op_NotIn:
// 至少一个值
if len(c.Value) < 1 {
return errors.New("value must contain at least one element for 'in' or 'not_in' operators")
}
case Op_Percent:
// 'percentage' 有且只有一个值且为0-100之间的整数
if len(c.Value) != 1 {
return errors.New("value for 'percentage' must contain exactly one element")
}
percent, err := strconv.Atoi(c.Value[0])
if err != nil {
return fmt.Errorf("value for 'percentage' must be a valid integer")
}
if percent < 0 || percent > 100 {
return fmt.Errorf("value for 'percentage' must be greater than 0 and less than 100")
}
default:
// 其他操作符只能有一个值
if len(c.Value) != 1 {
return fmt.Errorf("value must contain exactly one element for '%s' operator", c.Operator)
}
}
return nil
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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.
package main
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/tidwall/gjson"
)
func setDefaultTag(k string, v string, log wrapper.Log) {
if k == "" || v == "" {
return
}
addTagHeader(k, v, log)
}
func getFullRequestURL() (string, error) {
path, _ := proxywasm.GetHttpRequestHeader(":path")
return path, nil
}
func parseCookie(cookieHeader string, key string) (string, bool) {
cookies := strings.Split(cookieHeader, ";")
for _, cookie := range cookies {
cookie = strings.TrimSpace(cookie)
if strings.HasPrefix(cookie, key+"=") {
parts := strings.SplitN(cookie, "=", 2)
if len(parts) == 2 {
return parts[1], true
}
}
}
return "", false
}
func getQueryParameter(urlStr, paramKey string) (string, error) {
u, err := url.Parse(urlStr)
if err != nil {
return "", err
}
values, ok := u.Query()[paramKey]
if !ok {
return "", fmt.Errorf("parameter %s not found", paramKey)
}
return values[0], nil
}
func addTagHeader(key string, value string, log wrapper.Log) {
existValue, _ := proxywasm.GetHttpRequestHeader(key)
if existValue != "" {
log.Infof("ADD HEADER failed: %s already exists, value: %s", key, existValue)
return
}
if err := proxywasm.AddHttpRequestHeader(key, value); err != nil {
log.Infof("failed to add tag header: %s", err)
return
}
log.Infof("ADD HEADER: %s, value: %s", key, value)
}
func jsonValidate(json gjson.Result, log wrapper.Log) error {
if !json.Exists() {
log.Error("plugin config is missing in JSON")
return errors.New("plugin config is missing in JSON")
}
jsonStr := strings.TrimSpace(json.Raw)
if jsonStr == "{}" || jsonStr == "" {
log.Error("plugin config is empty")
return errors.New("plugin config is empty")
}
if !gjson.Valid(json.Raw) {
log.Error("plugin config is invalid JSON")
return errors.New("plugin config is invalid JSON")
}
return nil
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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.
package main
import (
"math/rand"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
)
func onWeightRequestHeaders(weightGroups []WeightGroup, randGen *rand.Rand, log wrapper.Log) bool {
if len(weightGroups) == 0 {
return false
}
randomValue := randGen.Uint64() % TotalWeight
log.Debugf("random value for weighted headers : %d", randomValue)
// CDF
for _, wg := range weightGroups {
if randomValue < uint64(wg.Accumulate) {
addTagHeader(wg.HeaderName, wg.HeaderValue, log)
return true
}
}
return false
}