Compare commits

..

118 Commits

Author SHA1 Message Date
Jun
a2c2d1d521 fix: fallbackForInvalidSecret to return original secret (#1245) 2024-08-25 15:59:12 +08:00
Yang
a5a28aebf6 Add x-forwarded-xxx for ext-auth (#1244) 2024-08-23 14:49:08 +08:00
YeHaitao
1c10f36369 feat: support 360 ai model (#1243)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-08-23 11:13:09 +08:00
韩贤涛
7054f01a36 feat: Adapt to the Qwen multimodal model generation API (#1221) 2024-08-22 18:42:16 +08:00
xingyunyang01
895f17f8d8 update: Add support for post tools, add round limits, per-round token… (#1230)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-08-22 16:33:42 +08:00
Pxl
29fcd330d5 feat: support ai-proxy custom settings (#1219) 2024-08-22 13:59:32 +08:00
Yang Beining
0e58042fa6 Support Openai structure output api (#feat 1214) (#1217)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-08-22 12:33:35 +08:00
brother-戎
bdbfad8a8a fix: fix up kingress controller NPE (#1235) 2024-08-22 09:59:55 +08:00
ran xuxin
4307f88645 extend ai-prompt-decorator plugin with client's geographic message from geo-ip plugin (#1228) 2024-08-20 16:14:21 +08:00
007gzs
25b085cb5e feat: ai敏感词拦截插件 (#1190) 2024-08-16 17:24:32 +08:00
urlyy
dcea483c61 Feat: Add Deepl support for plugins/ai-proxy (#1147) 2024-08-15 18:53:56 +08:00
rinfx
8fa1224cba support qwen compatible mode (#1205) 2024-08-15 18:52:49 +08:00
xingyunyang01
8f7c10ee5f feat: add ai-agent plugin (#1192) 2024-08-15 17:05:25 +08:00
澄潭
5a854b990b Update README.md 2024-08-15 09:53:02 +08:00
Jingze
dd11248e47 Update README.md (#1203) 2024-08-14 19:55:21 +08:00
mamba
ba98f3a7ad feat: 🎸 frontend-gray plugin support cdn type deploy (#1178)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-08-14 15:41:32 +08:00
Jun
d31c978ed3 feat: add AI quota plugin (#1200) 2024-08-14 13:43:31 +08:00
Jingze
daa374d9a4 feat: support wasm-assemblyscript sdk (#1175) 2024-08-13 15:31:36 +08:00
澄潭
6b9dabb489 Update README.md 2024-08-12 19:41:10 +08:00
rinfx
6f04404edd crash bugfix (#1198) 2024-08-12 16:42:10 +08:00
韩贤涛
04a9104062 feat: support gemini ai model (#1173) 2024-08-09 09:55:40 +08:00
Se7en
564f8c770a fix: fix tracing configmap template to handle initial installation (#1191) 2024-08-09 08:29:51 +08:00
Se7en
fec2e9dfc9 feat: improve Skywalking and Zipkin integration (#1131) 2024-08-08 22:40:33 +08:00
Jingze
dc4ddb52ee fix bug of empty config plugin still start (#1189) 2024-08-08 18:04:47 +08:00
Jun
6f221ead53 feat:add service rule match for wasmplugin in control panel (#1166) 2024-08-08 18:04:33 +08:00
韩贤涛
53f8410843 feat: ext auth forward_auth endpoint_mode enhancement (#1180) 2024-08-08 18:01:51 +08:00
rinfx
a17ac9e4c6 Optimize ai-rag plugin (#1170) 2024-08-08 18:00:02 +08:00
澄潭
5e95f6f057 Update README.md 2024-08-08 17:14:18 +08:00
澄潭
94f29e56c0 Update README.md 2024-08-08 17:12:33 +08:00
澄潭
870157c576 Update README.md 2024-08-08 15:53:21 +08:00
urlyy
c78ef7011d Feat: Add Spark llm support for plugins/ai-proxy (#1139) 2024-08-08 15:16:58 +08:00
澄潭
dc0dcaaaee azure-openai support other type api (#1187) 2024-08-08 13:33:12 +08:00
EricaLiu
34f5722d93 fix: add support for nacos triple protocol (#1186) 2024-08-08 10:29:48 +08:00
澄潭
55fdddee2f optimize transformer plugin (#1183) 2024-08-08 09:46:11 +08:00
007gzs
980ffde244 Optimize WASM Rust SDK's body caching logic. (#1181) 2024-08-07 20:06:11 +08:00
澄潭
0a578c2a04 ai-proxy: support custom openai provider (#1176)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-08-07 10:33:01 +08:00
澄潭
536a3069a8 Update README.md 2024-08-06 20:15:33 +08:00
韩贤涛
08c64ed467 fix:fix bug in ext-auth wasm plugin (#1152) 2024-08-05 11:04:31 +08:00
澄潭
cc74c0da93 replace regexp (#1169) 2024-07-31 17:48:38 +08:00
Kent Dong
210b97b06b fix: Use the official tinygo package to build Wasm go plugin builder (#1161) 2024-07-29 16:05:23 +08:00
007gzs
bccfbde621 fix PluginHttpWrapper 中 Context的回调未代理 . request-block case_sensitive 逻辑错误 (#1146) 2024-07-27 10:25:14 +08:00
澄潭
f1c6e78047 Update Makefile.core.mk 2024-07-26 14:06:38 +08:00
澄潭
1c415c60c3 rel: Release v1.4.2 (#1159) 2024-07-26 13:55:03 +08:00
澄潭
59acb61926 Update Makefile.core.mk 2024-07-26 13:50:16 +08:00
澄潭
29079f4e2a Support set buffer limit (#1153) 2024-07-25 20:42:39 +08:00
澄潭
95edce024d support custom trace span tag (#1156) 2024-07-25 20:42:09 +08:00
Kent Dong
b6d07a157c feat: Always buffer request body in ai-proxy plugin (#1155) 2024-07-25 19:35:39 +08:00
澄潭
10569f49ae support keep original auth header (#1151) 2024-07-24 19:31:38 +08:00
Jun
2a588c99c7 fix: add full push when higress-https configmap updated and fix certmagic storage (#1105) 2024-07-24 19:30:40 +08:00
Kent Dong
0cfef34bff feat: Support fallback route in ai-proxy plugin (#1123) 2024-07-24 19:25:32 +08:00
rinfx
5c2b5d5750 potential bug fix (#1141) 2024-07-24 19:24:47 +08:00
rinfx
8f483518a9 support take effect on api level (#1150) 2024-07-24 19:23:55 +08:00
Kent Dong
f6ee4ed166 fix: Bypass the response body processing in ai-proxy if it is returned internally (#1149) 2024-07-24 16:18:00 +08:00
Kent Dong
9a9e924037 feat: Make higress-core and higress-gateway as the default container (#1144) 2024-07-24 11:25:16 +08:00
jiaomh
e7d66f691f chore: Update multiple dependencies to the latest version (#1143) 2024-07-23 11:48:44 +08:00
rinfx
8c48fcb423 update template decorator (#1142) 2024-07-22 17:04:55 +08:00
007gzs
ef31e09310 feat: add rust demo plugin request block (#1091)
Co-authored-by: Yi <lynskylate@gmail.com>
2024-07-22 15:49:06 +08:00
韩贤涛
c0f2cafdc8 feat: support ext_auth wasmplugin (#1103) 2024-07-17 15:30:32 +08:00
Kent Dong
d5a9ff3a98 fix: Fix possible type-casting related panics in ai-proxy plugin (#1127) 2024-07-16 18:38:43 +08:00
Kent Dong
f069ad5b0d feat: Add statusCodeDetails info when returning response in Wasm plugins directly (#1116) 2024-07-16 09:52:46 +08:00
xu.zhao
85219b6c53 fix: controller has no right to watch deployment (#1089)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-15 16:34:24 +08:00
mamba
5041277be3 feat: 🎸 add frontend gray plugin (#1120)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-15 15:47:04 +08:00
rinfx
c00c8827f9 support service-level match config (#1112) 2024-07-15 14:00:02 +08:00
rinfx
46218058d1 token-ratelimit crash bugfix (#1119) 2024-07-12 15:05:15 +08:00
Kent Dong
5306385e6b feat: Support loading custom parameters in build-and-push-wasm-plugin-image.yaml (#1118) 2024-07-12 14:23:12 +08:00
Se7en
4e881fdd3f doc: update cluster-key-rate-limit doc (#1113)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-11 17:31:14 +08:00
Kent Dong
59aa3b5488 fix: Use "controller.name" to refer the controller service in higress-config (#1108) 2024-07-11 16:14:16 +08:00
Kent Dong
c40cf85aad fix: Fix the incorrect image name used in build-and-push-wasm-plugin-image.yaml (#1109) 2024-07-10 13:51:13 +08:00
Kent Dong
7c749b864c fix: Fix some bugs in build-and-push-wasm-plugin-image.yaml (#1107) 2024-07-10 13:41:58 +08:00
Yiiong
74ddbf02f6 feat:add build-and-push-wasm-plugin-image.yaml (#1069)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-08 21:44:58 +08:00
zzjin
60c56a16ab Support CredentialConfig.TLSSecret with namespace. Resolve: #1066 (#1095)
Signed-off-by: zzjin <tczzjin@gmail.com>
2024-07-08 19:49:51 +08:00
Kent Dong
5a2c6835f7 feat: Support embeddings API for Qwen in the ai-proxy plugin (#1079) 2024-07-08 19:37:08 +08:00
Kent Dong
12a5612450 feat: Support model prefix mapping in ai-proxy (#1097) 2024-07-08 19:33:08 +08:00
nohup
b9f5c4d1f2 feat: support Cloudflare Workers AI (#1068)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-08 19:27:11 +08:00
Jun
d7bdcbd026 fix priorityClassName missed (#1096) 2024-07-08 19:26:08 +08:00
野生程序员
dd284d1f24 feat: loadBalancerClass (#1071) 2024-07-08 10:58:33 +08:00
jiaomh
a7ee523c98 Update test/README.md (#1098) 2024-07-07 10:06:32 +08:00
Kent Dong
4bbfb131ee feat: Load 3rd-party images from higress image repo (#1067) 2024-07-04 20:14:00 +08:00
Se7en
6fd71f9749 fix: prometheus port (#1076) 2024-07-03 13:46:32 +08:00
pepesi
e0159f501a fix jwt-auth plugin claims_to_headers failed (#1075) 2024-07-03 10:11:17 +08:00
Kent Dong
56226d5052 feat: Create an IngressClass resource in the helm chart (#1072) 2024-07-02 21:22:00 +08:00
pepesi
086a9cc973 fixed ai-statistics plugin statistics error (#1060) 2024-07-02 20:35:12 +08:00
Tao Jikun
e389313aa3 feat: update doc for running Ingress API conformance tests (#1065)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-06-27 14:55:10 +08:00
澄潭
f64c601264 compatiable with openai sdk (#1061)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-06-27 09:30:52 +08:00
co63oc
9c6ea109f8 Fix typos (#1053) 2024-06-26 19:47:39 +08:00
yy
4ca2d23404 feat: helm charts support installing gateway in daemonset mod. (#1054) 2024-06-26 19:47:20 +08:00
co63oc
0ce52de59b Fix typos (#1050) 2024-06-22 16:22:03 +08:00
澄潭
81e459da01 Update Makefile.core.mk 2024-06-19 17:40:22 +08:00
澄潭
63539ca15c rel:Release v1.4.1 (#1048) 2024-06-19 17:12:51 +08:00
澄潭
1eea75f130 Update Makefile.core.mk 2024-06-19 17:00:06 +08:00
The Wind
d333656cc3 feat: support summary output for route/cluster/listener in hgctl gateway-config command (#995) (#996) 2024-06-19 13:55:59 +08:00
Se7en
51dca7055a feat: support claude ai model (#969)
Signed-off-by: chengzw <chengzw258@163.com>
2024-06-19 13:53:21 +08:00
Chi Kai
ab1bc0a73a feat: support stepfun model (#1012)
Co-authored-by: Kent Dong <kentstl@163.com>
2024-06-19 13:51:02 +08:00
Kent Dong
ffee7dc5ea fix: Accommodate the incomplete function name in the initial event from Qwen (#1045) 2024-06-19 13:48:20 +08:00
rinfx
1ea87f0e7a add plugin: ai-token-ratelimit (#1015) 2024-06-19 13:46:59 +08:00
rinfx
7164653446 ai rag updates (#1046) 2024-06-19 13:46:17 +08:00
澄潭
2a1a391054 Optimize the effectiveness speed of xds and add AI-related metric tags (#1047) 2024-06-19 13:45:42 +08:00
rinfx
0785d4aac4 add plugin: ai-statistics (#1011) 2024-06-18 17:53:04 +08:00
rinfx
4ca4bec2b5 add plugin: ai-transformer (#1035) 2024-06-18 17:51:38 +08:00
rinfx
174350d3fb add plugin: ai-rag (#1038) 2024-06-17 15:37:00 +08:00
rinfx
0380cb03d3 add plugin: ai-prompt-template (#1019) 2024-06-17 15:33:05 +08:00
rinfx
15d9f76ff9 add plugin: ai-prompt-decorator (#1021) 2024-06-17 15:31:34 +08:00
rinfx
5f15017963 add plugin: ai-security-guard (#1034) 2024-06-17 10:41:46 +08:00
韩贤涛
634de3f7f8 feat: cluster key rate limit enhancement (#1036) 2024-06-17 10:37:03 +08:00
韩贤涛
12cc44b324 feat: cluster key rate limit (#1002) 2024-06-12 14:51:46 +08:00
韩贤涛
d53c713561 feat: support minimax ai model (#1033) 2024-06-09 21:01:31 +08:00
澄潭
5acc6f73b2 fix prometheus stats (#1031) 2024-06-07 17:24:19 +08:00
韩贤涛
2db0b60a98 feat: support baidu ernie bot ai model (#1024)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-06-06 18:19:55 +08:00
nash5
c6e3db95e0 feature: add hunyuan llm support for plugins/ai-proxy (#1018)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-06-06 18:11:51 +08:00
Ink33
ed976c6d06 feat(plugin): implement golang version of plugin jwt-auth (#743)
Signed-off-by: Ink33 <Ink33@smlk.org>
2024-06-06 10:22:51 +08:00
Jun
6a40d83ec0 enable automatichttps (#1026) 2024-06-04 09:35:30 +08:00
Jun
2807ddfbb7 Feat https fallback (#1020) 2024-05-31 14:04:02 +08:00
Kent Dong
6e4ade05a8 doc: Add instructions of how to build ai-proxy plugin (#1005) 2024-05-31 13:59:43 +08:00
Kent Dong
bdd050b926 fix: Fix tool_calls compatibility issues with LobeChat (#1009) 2024-05-30 19:20:48 +08:00
澄潭
38ddc49360 Optimize the method for judging streaming responses (#1017) 2024-05-30 17:50:28 +08:00
澄潭
26ec0d3d55 Update manifests.yaml 2024-05-30 10:42:54 +08:00
澄潭
909f8bc719 Update main.go 2024-05-30 10:26:52 +08:00
澄潭
863d0e5872 Update main.go 2024-05-30 09:53:48 +08:00
340 changed files with 87862 additions and 963 deletions

View File

@@ -0,0 +1,114 @@
name: Build and Push Wasm Plugin Image
on:
push:
tags:
- "wasm-go-*-v*.*.*" # 匹配 wasm-go-{pluginName}-vX.Y.Z 格式的标签
workflow_dispatch:
inputs:
plugin_name:
description: 'Name of the plugin'
required: true
type: string
version:
description: 'Version of the plugin (optional, without leading v)'
required: false
type: string
jobs:
build-and-push-wasm-plugin-image:
runs-on: ubuntu-latest
environment:
name: image-registry-msg
env:
IMAGE_REGISTRY_SERVICE: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
IMAGE_REPOSITORY: ${{ vars.PLUGIN_IMAGE_REPOSITORY || 'plugins' }}
GO_VERSION: 1.19
TINYGO_VERSION: 0.28.1
ORAS_VERSION: 1.0.0
steps:
- name: Set plugin_name and version from inputs or ref_name
id: set_vars
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
plugin_name="${{ github.event.inputs.plugin_name }}"
version="${{ github.event.inputs.version }}"
else
ref_name=${{ github.ref_name }}
plugin_name=${ref_name#*-*-} # 删除插件名前面的字段(wasm-go-)
plugin_name=${plugin_name%-*} # 删除插件名后面的字段(-vX.Y.Z)
version=$(echo "$ref_name" | awk -F'v' '{print $2}')
fi
echo "PLUGIN_NAME=$plugin_name" >> $GITHUB_ENV
echo "VERSION=$version" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v3
- name: File Check
run: |
workspace=${{ github.workspace }}/plugins/wasm-go/extensions/${PLUGIN_NAME}
push_command="./plugin.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip"
# 查找spec.yaml
if [ -f "${workspace}/spec.yaml" ]; then
echo "spec.yaml exists"
push_command="./spec.yaml:application/vnd.module.wasm.spec.v1+yaml $push_command "
fi
# 查找README.md
if [ -f "${workspace}/README.md" ];then
echo "README.md exists"
push_command="./README.md:application/vnd.module.wasm.doc.v1+markdown $push_command "
fi
# 查找README_{lang}.md
for file in ${workspace}/README_*.md; do
if [ -f "$file" ]; then
file_name=$(basename $file)
echo "$file_name exists"
lang=$(basename $file | sed 's/README_//; s/.md//')
push_command="./$file_name:application/vnd.module.wasm.doc.v1.$lang+markdown $push_command "
fi
done
echo "PUSH_COMMAND=\"$push_command\"" >> $GITHUB_ENV
- name: Run a wasm-go-builder
env:
PLUGIN_NAME: ${{ env.PLUGIN_NAME }}
BUILDER_IMAGE: higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-tinygo${{ env.TINYGO_VERSION }}-oras${{ env.ORAS_VERSION }}
run: |
docker run -itd --name builder -v ${{ github.workspace }}:/workspace -e PLUGIN_NAME=${{ env.PLUGIN_NAME }} --rm ${{ env.BUILDER_IMAGE }} /bin/bash
- name: Build Image and Push
run: |
push_command=${{ env.PUSH_COMMAND }}
push_command=${push_command#\"}
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号确保oras push正常解析
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
echo "TargetImage=${target_image}"
cd ${{ github.workspace }}/plugins/wasm-go/extensions/${PLUGIN_NAME}
if [ -f ./.buildrc ]; then
echo 'Found .buildrc file, sourcing it...'
. ./.buildrc
else
echo '.buildrc file not found'
fi
echo "EXTRA_TAGS=${EXTRA_TAGS}"
command="
set -e
cd /workspace/plugins/wasm-go/extensions/${PLUGIN_NAME}
go mod tidy
tinygo build -o ./plugin.wasm -scheduler=none -target=wasi -gc=custom -tags=\"custommalloc nottinygc_finalizer ${EXTRA_TAGS}\" .
tar czvf plugin.tar.gz plugin.wasm
echo ${{ secrets.REGISTRY_PASSWORD }} | oras login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ env.IMAGE_REGISTRY_SERVICE }}
oras push ${target_image} ${push_command}
"
docker exec builder bash -c "$command"

View File

@@ -17,8 +17,8 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.19
# There are too many lint errors in current code bases
@@ -30,9 +30,9 @@ jobs:
strategy:
matrix:
# TODO(Xunzhuo): Enable C WASM Filters in CI
wasmPluginType: [ GO ]
wasmPluginType: [ GO, RUST ]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
@@ -45,12 +45,17 @@ jobs:
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
if: matrix.wasmPluginType == 'RUST'
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -60,7 +65,7 @@ jobs:
${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
.git/modules
@@ -81,4 +86,4 @@ jobs:
runs-on: ubuntu-latest
needs: [ higress-wasmplugin-test ]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

View File

@@ -10,8 +10,8 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.19
# There are too many lint errors in current code bases
@@ -21,10 +21,10 @@ jobs:
coverage-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -33,7 +33,7 @@ jobs:
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
.git/modules
@@ -46,7 +46,7 @@ jobs:
- name: Run Coverage Tests
run: GOPROXY="https://proxy.golang.org,direct" make go.test.coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: false
files: ./coverage.xml
@@ -58,17 +58,17 @@ jobs:
needs: [lint,coverage-test]
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -77,7 +77,7 @@ jobs:
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
.git/modules
@@ -90,7 +90,7 @@ jobs:
run: GOPROXY="https://proxy.golang.org,direct" make build
- name: Upload Higress Binary
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: higress
path: out/
@@ -108,12 +108,12 @@ jobs:
- uses: actions/checkout@v3
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -123,7 +123,7 @@ jobs:
${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
.git/modules
@@ -139,4 +139,4 @@ jobs:
runs-on: ubuntu-latest
needs: [higress-conformance-test,gateway-conformance-test]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

View File

@@ -16,7 +16,7 @@ jobs:
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -31,12 +31,12 @@ jobs:
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -45,7 +45,7 @@ jobs:
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
envoy
@@ -56,7 +56,7 @@ jobs:
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
@@ -67,7 +67,7 @@ jobs:
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -92,7 +92,7 @@ jobs:
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -107,12 +107,12 @@ jobs:
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -121,7 +121,7 @@ jobs:
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
envoy
@@ -132,7 +132,7 @@ jobs:
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
@@ -143,7 +143,7 @@ jobs:
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -169,7 +169,7 @@ jobs:
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -184,12 +184,12 @@ jobs:
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
@@ -198,7 +198,7 @@ jobs:
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |-
envoy
@@ -209,7 +209,7 @@ jobs:
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
@@ -220,7 +220,7 @@ jobs:
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}

View File

@@ -34,11 +34,11 @@ jobs:
steps:
# step 1
- name: "Checkout repository"
uses: actions/checkout@v2
uses: actions/checkout@v4
# step 2: Initializes the CodeQL tools for scanning.
- name: "Initialize CodeQL"
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: "Autobuild"
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# step 4
# Command-line programs to run using the OS shell.
@@ -66,4 +66,4 @@ jobs:
# step 5
- name: "Perform CodeQL Analysis"
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -14,7 +14,7 @@ jobs:
steps:
# Step 1
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# Step 2
- id: package
name: Prepare Standalone Package

View File

@@ -14,7 +14,7 @@ jobs:
steps:
# Step 1
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# Step 2
- name: Download Helm Charts Index
uses: doggycool/ossutil-github-action@master

View File

@@ -9,7 +9,7 @@ jobs:
latest-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build hgctl latest multiarch binaries
run: |
@@ -46,7 +46,7 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository_owner }}/${{ github.event.repository.name }}
- name: Recreate the Latest Release and Tag
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
draft: false
prerelease: true

View File

@@ -10,7 +10,7 @@ jobs:
steps:
# step 1
- name: Checkout
uses: actions/checkout@v2.4.0
uses: actions/checkout@v4
# step 2
- name: Check License Header
uses: apache/skywalking-eyes/header@25edfc2fd8d52fb266653fb5f6c42da633d85c07
@@ -24,4 +24,4 @@ jobs:
with:
log: info
config: .licenserc.yaml
mode: check
mode: check

View File

@@ -12,7 +12,7 @@ jobs:
env:
HGCTL_VERSION: ${{github.ref_name}}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build hgctl latest multiarch binaries
run: |
@@ -25,7 +25,7 @@ jobs:
zip -q -r hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip out/windows_arm64/
- name: Upload hgctl packages to the GitHub release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
@@ -34,4 +34,4 @@ jobs:
hgctl_${{ env.HGCTL_VERSION }}_darwin_amd64.tar.gz
hgctl_${{ env.HGCTL_VERSION }}_darwin_arm64.tar.gz
hgctl_${{ env.HGCTL_VERSION }}_windows_amd64.zip
hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip
hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip

View File

@@ -138,11 +138,11 @@ export ENVOY_TAR_PATH:=/home/package/envoy.tar.gz
external/package/envoy-amd64.tar.gz:
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
cd external/package; wget -O envoy-amd64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.0-rc.1/envoy-symbol-amd64.tar.gz"
cd external/package; wget -O envoy-amd64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.1/envoy-symbol-amd64.tar.gz"
external/package/envoy-arm64.tar.gz:
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
cd external/package; wget -O envoy-arm64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.0-rc.1/envoy-symbol-arm64.tar.gz"
cd external/package; wget -O envoy-arm64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.1/envoy-symbol-arm64.tar.gz"
build-pilot:
cd external/istio; rm -rf out/linux_amd64; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=amd64 BUILD_WITH_CONTAINER=1 make build-linux
@@ -177,8 +177,8 @@ install: pre-install
cd helm/higress; helm dependency build
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
ENVOY_LATEST_IMAGE_TAG ?= sha-93966bf
ISTIO_LATEST_IMAGE_TAG ?= sha-b00f79f
ENVOY_LATEST_IMAGE_TAG ?= sha-59acb61
ISTIO_LATEST_IMAGE_TAG ?= sha-59acb61
install-dev: pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'

View File

@@ -1,17 +1,18 @@
<h1 align="center">
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
<br>
Cloud Native API Gateway
AI Gateway
</h1>
<h4 align="center"> AI Native API Gateway </h4>
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[**官网**](https://higress.io/) &nbsp; |
&nbsp; [**文档**](https://higress.io/zh-cn/docs/overview/what-is-higress) &nbsp; |
&nbsp; [**博客**](https://higress.io/zh-cn/blog) &nbsp; |
&nbsp; [**开发指引**](https://higress.io/zh-cn/docs/developers/developers_dev) &nbsp; |
&nbsp; [**Higress 企业版**](https://www.aliyun.com/product/aliware/mse?spm=higress-website.topbar.0.0.0) &nbsp;
&nbsp; [**文档**](https://higress.io/docs/latest/user/quickstart/) &nbsp; |
&nbsp; [**博客**](https://higress.io/blog/) &nbsp; |
&nbsp; [**开发指引**](https://higress.io/docs/latest/dev/architecture/) &nbsp; |
&nbsp; [**AI插件**](https://higress.io/plugin/) &nbsp;
<p>
@@ -19,21 +20,54 @@
</p>
Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。Higress 实现了安全防护网关、流量网关、微服务网关三层网关合一,可以显著降低网关的部署和运维成本。
Higress 是基于阿里内部多的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。
Higress 在阿里内部作为 AI 网关,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
![arch](https://img.alicdn.com/imgextra/i1/O1CN01iO9ph825juHbOIg75_!!6000000007563-2-tps-2483-2024.png)
## Summary
- [**快速开始**](#快速开始)
- [**功能展示**](#功能展示)
- [**使用场景**](#使用场景)
- [**核心优势**](#核心优势)
- [**Quick Start**](https://higress.io/zh-cn/docs/user/quickstart)
- [**社区**](#社区)
## 快速开始
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
```bash
# 创建一个工作目录
mkdir higress; cd higress
# 启动 higress配置文件会写到工作目录下
docker run -d --rm --name higress-ai -v ${PWD}:/data \
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
```
监听端口说明如下:
- 8001 端口Higress UI 控制台入口
- 8080 端口:网关 HTTP 协议入口
- 8443 端口:网关 HTTPS 协议入口
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内不可访问的影响**
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.io/docs/latest/user/quickstart/)。
## 使用场景
- **AI 网关**:
Higress 提供了一站式的 AI 插件集,可以增强依赖 AI 能力业务的稳定性、灵活性、可观测性,使得业务与 AI 的集成更加便捷和高效。
- **Kubernetes Ingress 网关**:
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
@@ -56,27 +90,36 @@ Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源
脱胎于阿里巴巴2年多生产验证的内部产品支持每秒请求量达数十万级的大规模场景。
彻底摆脱 reload 引起的流量抖动,配置变更毫秒级生效且业务无感。
- **平滑演进**
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
支持 Nacos/Zookeeper/Eureka 等多种注册中心,可以不依赖 K8s Service 进行服务发现,支持非容器架构平滑演进到云原生架构。
- **流式处理**
支持从 Nginx Ingress Controller 平滑迁移,支持平滑过渡到 Gateway API支持业务架构平滑演进到 ServiceMesh
支持真正的完全流式处理请求/响应 BodyWasm 插件很方便地自定义处理 SSE Server-Sent Events等流式协议的报文
- **兼收并蓄**
兼容 Nginx Ingress Annotation 80%+ 的使用场景,且提供功能更丰富的 Higress Annotation 注解。
兼容 Ingress API/Gateway API/Istio API可以组合多种 CRD 实现流量精细化管理。
在 AI 业务等大带宽场景下,可以显著降低内存开销。
- **便于扩展**
提供 Wasm、Lua、进程外三种插件扩展机制支持多语言编写插件生效粒度支持全局级、域名级路由级
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能满足90%以上的业务场景需求
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
- **安全易用**
基于 Ingress API 和 Gateway API 标准,提供开箱即用的 UI 控制台WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
插件支持热更新,变更插件逻辑和配置都对流量无损。
## 功能展示
### AI 网关 Demo 展示
[从 OpenAI 到其他大模型30 秒完成迁移
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
### Higress UI 控制台
- **丰富的可观测**

View File

@@ -1 +1 @@
v1.4.0
v1.4.2

View File

@@ -301,6 +301,7 @@ type MatchRule struct {
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
Config *types.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
ConfigDisable bool `protobuf:"varint,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@@ -367,6 +368,13 @@ func (m *MatchRule) GetConfigDisable() bool {
return false
}
func (m *MatchRule) GetService() []string {
if m != nil {
return m.Service
}
return nil
}
func init() {
proto.RegisterEnum("higress.extensions.v1alpha1.PluginPhase", PluginPhase_name, PluginPhase_value)
proto.RegisterEnum("higress.extensions.v1alpha1.PullPolicy", PullPolicy_name, PullPolicy_value)
@@ -377,46 +385,47 @@ func init() {
func init() { proto.RegisterFile("extensions/v1alpha1/wasm.proto", fileDescriptor_4d60b240916c4e18) }
var fileDescriptor_4d60b240916c4e18 = []byte{
// 619 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0xdd, 0x4e, 0xdb, 0x4c,
0x10, 0x86, 0x71, 0x02, 0x81, 0x4c, 0x80, 0xcf, 0xac, 0xbe, 0xd2, 0x15, 0x54, 0x69, 0x84, 0xd4,
0xd6, 0xe5, 0xc0, 0x16, 0xa1, 0x3f, 0x27, 0x15, 0x6a, 0x80, 0xb4, 0x44, 0x6d, 0x53, 0xcb, 0x86,
0x56, 0xe5, 0xc4, 0xda, 0x98, 0x8d, 0xb3, 0xea, 0xfa, 0x47, 0xde, 0x35, 0x34, 0x17, 0xd2, 0x7b,
0xea, 0x61, 0x2f, 0xa1, 0xe2, 0x2e, 0x7a, 0x56, 0x65, 0x6d, 0x43, 0x42, 0xab, 0x9c, 0xed, 0xce,
0x3c, 0x33, 0xf3, 0xbe, 0xe3, 0x95, 0xa1, 0x49, 0xbf, 0x49, 0x1a, 0x09, 0x16, 0x47, 0xc2, 0xba,
0xdc, 0x23, 0x3c, 0x19, 0x91, 0x3d, 0xeb, 0x8a, 0x88, 0xd0, 0x4c, 0xd2, 0x58, 0xc6, 0x68, 0x7b,
0xc4, 0x82, 0x94, 0x0a, 0x61, 0xde, 0x72, 0x66, 0xc9, 0x6d, 0x35, 0x83, 0x38, 0x0e, 0x38, 0xb5,
0x14, 0x3a, 0xc8, 0x86, 0xd6, 0x55, 0x4a, 0x92, 0x84, 0xa6, 0x22, 0x2f, 0xde, 0x7a, 0x70, 0x37,
0x2f, 0x64, 0x9a, 0xf9, 0x32, 0xcf, 0xee, 0xfc, 0x5e, 0x04, 0xf8, 0x4c, 0x44, 0x68, 0xf3, 0x2c,
0x60, 0x11, 0xd2, 0xa1, 0x9a, 0xa5, 0x1c, 0x57, 0x5a, 0x9a, 0x51, 0x77, 0x26, 0x47, 0xb4, 0x09,
0x35, 0x31, 0x22, 0xed, 0xe7, 0x2f, 0x70, 0x55, 0x05, 0x8b, 0x1b, 0x72, 0x61, 0x83, 0x85, 0x24,
0xa0, 0x5e, 0x92, 0x71, 0xee, 0x25, 0x31, 0x67, 0xfe, 0x18, 0x2f, 0xb6, 0x34, 0x63, 0xbd, 0xfd,
0xc4, 0x9c, 0xa3, 0xd7, 0xb4, 0x33, 0xce, 0x6d, 0x85, 0x3b, 0xff, 0xa9, 0x0e, 0xb7, 0x01, 0xb4,
0x3b, 0xd3, 0x54, 0x50, 0x3f, 0xa5, 0x12, 0x2f, 0xa9, 0xb9, 0xb7, 0xac, 0xab, 0xc2, 0xe8, 0x29,
0xe8, 0x97, 0x34, 0x65, 0x43, 0xe6, 0x13, 0xc9, 0xe2, 0xc8, 0xfb, 0x4a, 0xc7, 0xb8, 0x96, 0xa3,
0xd3, 0xf1, 0x77, 0x74, 0x8c, 0x5e, 0xc1, 0x5a, 0xa2, 0xfc, 0x79, 0x7e, 0x1c, 0x0d, 0x59, 0x80,
0x97, 0x5b, 0x9a, 0xd1, 0x68, 0xdf, 0x37, 0xf3, 0xd5, 0x98, 0xe5, 0x6a, 0x4c, 0x57, 0xad, 0xc6,
0x59, 0xcd, 0xe9, 0x23, 0x05, 0xa3, 0x87, 0xd0, 0x28, 0xaa, 0x23, 0x12, 0x52, 0xbc, 0xa2, 0x66,
0x40, 0x1e, 0xea, 0x93, 0x90, 0xa2, 0x03, 0x58, 0x4a, 0x46, 0x44, 0x50, 0x5c, 0x57, 0xf6, 0x8d,
0xf9, 0xf6, 0x55, 0x9d, 0x3d, 0xe1, 0x9d, 0xbc, 0x0c, 0xbd, 0x84, 0x95, 0x24, 0x65, 0x71, 0xca,
0xe4, 0x18, 0x83, 0x52, 0xb6, 0xfd, 0x97, 0xb2, 0x5e, 0x24, 0xf7, 0xdb, 0x9f, 0x08, 0xcf, 0xa8,
0x73, 0x03, 0xa3, 0x03, 0x58, 0xbf, 0xa0, 0x43, 0x92, 0x71, 0x59, 0x1a, 0xa3, 0xf3, 0x8d, 0xad,
0x15, 0x78, 0xe1, 0xec, 0x2d, 0x34, 0x42, 0x22, 0xfd, 0x91, 0x97, 0x66, 0x9c, 0x0a, 0x3c, 0x6c,
0x55, 0x8d, 0x46, 0xfb, 0xf1, 0x5c, 0xf9, 0x1f, 0x26, 0xbc, 0x93, 0x71, 0xea, 0x40, 0x58, 0x1e,
0x05, 0x7a, 0x06, 0x9b, 0xb3, 0x42, 0xbc, 0x0b, 0x26, 0xc8, 0x80, 0x53, 0x1c, 0xb4, 0x34, 0x63,
0xc5, 0xf9, 0x7f, 0x66, 0xee, 0x71, 0x9e, 0xdb, 0xf9, 0xae, 0x41, 0xfd, 0xa6, 0x1f, 0xc2, 0xb0,
0xcc, 0x22, 0x35, 0x18, 0x6b, 0xad, 0xaa, 0x51, 0x77, 0xca, 0xeb, 0xe4, 0x09, 0x5e, 0xc4, 0x21,
0x61, 0x11, 0xae, 0xa8, 0x44, 0x71, 0x43, 0x16, 0xd4, 0x0a, 0xdb, 0xd5, 0xf9, 0xb6, 0x0b, 0x0c,
0x3d, 0x82, 0xf5, 0x3b, 0xf2, 0x16, 0x95, 0xbc, 0x35, 0x7f, 0x5a, 0xd7, 0x6e, 0x17, 0x1a, 0x53,
0x5f, 0x09, 0xdd, 0x83, 0x8d, 0xb3, 0xbe, 0x6b, 0x77, 0x8f, 0x7a, 0x6f, 0x7a, 0xdd, 0x63, 0xcf,
0x3e, 0xe9, 0xb8, 0x5d, 0x7d, 0x01, 0xd5, 0x61, 0xa9, 0x73, 0x76, 0x7a, 0xd2, 0xd7, 0xb5, 0xf2,
0x78, 0xae, 0x57, 0x26, 0x47, 0xf7, 0xb4, 0x73, 0xea, 0xea, 0xd5, 0xdd, 0x43, 0x80, 0xa9, 0xa7,
0xbd, 0x09, 0x68, 0xa6, 0xcb, 0xc7, 0xf7, 0xbd, 0xa3, 0x2f, 0xfa, 0x02, 0xd2, 0x61, 0xb5, 0x37,
0xec, 0xc7, 0xd2, 0x4e, 0xa9, 0xa0, 0x91, 0xd4, 0x35, 0x04, 0x50, 0xeb, 0xf0, 0x2b, 0x32, 0x16,
0x7a, 0xe5, 0xf0, 0xf5, 0x8f, 0xeb, 0xa6, 0xf6, 0xf3, 0xba, 0xa9, 0xfd, 0xba, 0x6e, 0x6a, 0xe7,
0xed, 0x80, 0xc9, 0x51, 0x36, 0x30, 0xfd, 0x38, 0xb4, 0x08, 0x67, 0x03, 0x32, 0x20, 0x56, 0xf1,
0xb1, 0x2c, 0x92, 0x30, 0xeb, 0x1f, 0xbf, 0x91, 0x41, 0x4d, 0x2d, 0x63, 0xff, 0x4f, 0x00, 0x00,
0x00, 0xff, 0xff, 0xb9, 0xf2, 0x67, 0xbe, 0x64, 0x04, 0x00, 0x00,
// 631 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0xdd, 0x6e, 0xd3, 0x4c,
0x10, 0x86, 0xeb, 0xa4, 0x49, 0x9b, 0x49, 0xdb, 0xcf, 0x5d, 0x7d, 0x94, 0x55, 0x8b, 0x42, 0x54,
0x09, 0x30, 0x3d, 0xb0, 0xd5, 0x94, 0x9f, 0x13, 0x54, 0x91, 0xb6, 0x81, 0x46, 0x40, 0xb0, 0xec,
0x16, 0x44, 0x4f, 0xac, 0x8d, 0xbb, 0x71, 0x56, 0xac, 0x7f, 0xe4, 0x5d, 0xb7, 0xe4, 0xaa, 0xb8,
0x0d, 0x0e, 0xb9, 0x04, 0xd4, 0xbb, 0xe0, 0x0c, 0x65, 0xed, 0x34, 0x49, 0x41, 0x39, 0xdb, 0x9d,
0x79, 0x66, 0xe6, 0x7d, 0xc7, 0x2b, 0x43, 0x83, 0x7e, 0x93, 0x34, 0x12, 0x2c, 0x8e, 0x84, 0x75,
0xb5, 0x4f, 0x78, 0x32, 0x24, 0xfb, 0xd6, 0x35, 0x11, 0xa1, 0x99, 0xa4, 0xb1, 0x8c, 0xd1, 0xce,
0x90, 0x05, 0x29, 0x15, 0xc2, 0x9c, 0x72, 0xe6, 0x84, 0xdb, 0x6e, 0x04, 0x71, 0x1c, 0x70, 0x6a,
0x29, 0xb4, 0x9f, 0x0d, 0xac, 0xeb, 0x94, 0x24, 0x09, 0x4d, 0x45, 0x5e, 0xbc, 0xfd, 0xe0, 0x6e,
0x5e, 0xc8, 0x34, 0xf3, 0x65, 0x9e, 0xdd, 0xfd, 0xbd, 0x0c, 0xf0, 0x99, 0x88, 0xd0, 0xe6, 0x59,
0xc0, 0x22, 0xa4, 0x43, 0x39, 0x4b, 0x39, 0x2e, 0x35, 0x35, 0xa3, 0xe6, 0x8c, 0x8f, 0x68, 0x0b,
0xaa, 0x62, 0x48, 0x5a, 0xcf, 0x5f, 0xe0, 0xb2, 0x0a, 0x16, 0x37, 0xe4, 0xc2, 0x26, 0x0b, 0x49,
0x40, 0xbd, 0x24, 0xe3, 0xdc, 0x4b, 0x62, 0xce, 0xfc, 0x11, 0x5e, 0x6e, 0x6a, 0xc6, 0x46, 0xeb,
0x89, 0xb9, 0x40, 0xaf, 0x69, 0x67, 0x9c, 0xdb, 0x0a, 0x77, 0xfe, 0x53, 0x1d, 0xa6, 0x01, 0xb4,
0x37, 0xd7, 0x54, 0x50, 0x3f, 0xa5, 0x12, 0x57, 0xd4, 0xdc, 0x29, 0xeb, 0xaa, 0x30, 0x7a, 0x0a,
0xfa, 0x15, 0x4d, 0xd9, 0x80, 0xf9, 0x44, 0xb2, 0x38, 0xf2, 0xbe, 0xd2, 0x11, 0xae, 0xe6, 0xe8,
0x6c, 0xfc, 0x1d, 0x1d, 0xa1, 0x57, 0xb0, 0x9e, 0x28, 0x7f, 0x9e, 0x1f, 0x47, 0x03, 0x16, 0xe0,
0x95, 0xa6, 0x66, 0xd4, 0x5b, 0xf7, 0xcd, 0x7c, 0x35, 0xe6, 0x64, 0x35, 0xa6, 0xab, 0x56, 0xe3,
0xac, 0xe5, 0xf4, 0xb1, 0x82, 0xd1, 0x43, 0xa8, 0x17, 0xd5, 0x11, 0x09, 0x29, 0x5e, 0x55, 0x33,
0x20, 0x0f, 0xf5, 0x48, 0x48, 0xd1, 0x21, 0x54, 0x92, 0x21, 0x11, 0x14, 0xd7, 0x94, 0x7d, 0x63,
0xb1, 0x7d, 0x55, 0x67, 0x8f, 0x79, 0x27, 0x2f, 0x43, 0x2f, 0x61, 0x35, 0x49, 0x59, 0x9c, 0x32,
0x39, 0xc2, 0xa0, 0x94, 0xed, 0xfc, 0xa5, 0xac, 0x1b, 0xc9, 0x83, 0xd6, 0x27, 0xc2, 0x33, 0xea,
0xdc, 0xc2, 0xe8, 0x10, 0x36, 0x2e, 0xe9, 0x80, 0x64, 0x5c, 0x4e, 0x8c, 0xd1, 0xc5, 0xc6, 0xd6,
0x0b, 0xbc, 0x70, 0xf6, 0x16, 0xea, 0x21, 0x91, 0xfe, 0xd0, 0x4b, 0x33, 0x4e, 0x05, 0x1e, 0x34,
0xcb, 0x46, 0xbd, 0xf5, 0x78, 0xa1, 0xfc, 0x0f, 0x63, 0xde, 0xc9, 0x38, 0x75, 0x20, 0x9c, 0x1c,
0x05, 0x7a, 0x06, 0x5b, 0xf3, 0x42, 0xbc, 0x4b, 0x26, 0x48, 0x9f, 0x53, 0x1c, 0x34, 0x35, 0x63,
0xd5, 0xf9, 0x7f, 0x6e, 0xee, 0x49, 0x9e, 0xdb, 0xfd, 0xae, 0x41, 0xed, 0xb6, 0x1f, 0xc2, 0xb0,
0xc2, 0x22, 0x35, 0x18, 0x6b, 0xcd, 0xb2, 0x51, 0x73, 0x26, 0xd7, 0xf1, 0x13, 0xbc, 0x8c, 0x43,
0xc2, 0x22, 0x5c, 0x52, 0x89, 0xe2, 0x86, 0x2c, 0xa8, 0x16, 0xb6, 0xcb, 0x8b, 0x6d, 0x17, 0x18,
0x7a, 0x04, 0x1b, 0x77, 0xe4, 0x2d, 0x2b, 0x79, 0xeb, 0xfe, 0xac, 0xae, 0xb1, 0x12, 0x41, 0xd3,
0x2b, 0xe6, 0x53, 0x5c, 0xc9, 0x95, 0x14, 0xd7, 0xbd, 0x0e, 0xd4, 0x67, 0xbe, 0x1f, 0xba, 0x07,
0x9b, 0xe7, 0x3d, 0xd7, 0xee, 0x1c, 0x77, 0xdf, 0x74, 0x3b, 0x27, 0x9e, 0x7d, 0xda, 0x76, 0x3b,
0xfa, 0x12, 0xaa, 0x41, 0xa5, 0x7d, 0x7e, 0x76, 0xda, 0xd3, 0xb5, 0xc9, 0xf1, 0x42, 0x2f, 0x8d,
0x8f, 0xee, 0x59, 0xfb, 0xcc, 0xd5, 0xcb, 0x7b, 0x47, 0x00, 0x33, 0x8f, 0x7e, 0x0b, 0xd0, 0x5c,
0x97, 0x8f, 0xef, 0xbb, 0xc7, 0x5f, 0xf4, 0x25, 0xa4, 0xc3, 0x5a, 0x77, 0xd0, 0x8b, 0xa5, 0x9d,
0x52, 0x41, 0x23, 0xa9, 0x6b, 0x08, 0xa0, 0xda, 0xe6, 0xd7, 0x64, 0x24, 0xf4, 0xd2, 0xd1, 0xeb,
0x1f, 0x37, 0x0d, 0xed, 0xe7, 0x4d, 0x43, 0xfb, 0x75, 0xd3, 0xd0, 0x2e, 0x5a, 0x01, 0x93, 0xc3,
0xac, 0x6f, 0xfa, 0x71, 0x68, 0x11, 0xce, 0xfa, 0xa4, 0x4f, 0xac, 0xe2, 0x33, 0x5a, 0x24, 0x61,
0xd6, 0x3f, 0x7e, 0x30, 0xfd, 0xaa, 0x5a, 0xd3, 0xc1, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0b,
0x3c, 0xc3, 0xcf, 0x7e, 0x04, 0x00, 0x00,
}
func (m *WasmPlugin) Marshal() (dAtA []byte, err error) {
@@ -581,6 +590,15 @@ func (m *MatchRule) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if len(m.Service) > 0 {
for iNdEx := len(m.Service) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Service[iNdEx])
copy(dAtA[i:], m.Service[iNdEx])
i = encodeVarintWasm(dAtA, i, uint64(len(m.Service[iNdEx])))
i--
dAtA[i] = 0x2a
}
}
if m.ConfigDisable {
i--
if m.ConfigDisable {
@@ -719,6 +737,12 @@ func (m *MatchRule) Size() (n int) {
if m.ConfigDisable {
n += 2
}
if len(m.Service) > 0 {
for _, s := range m.Service {
l = len(s)
n += 1 + l + sovWasm(uint64(l))
}
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -1291,6 +1315,38 @@ func (m *MatchRule) Unmarshal(dAtA []byte) error {
}
}
m.ConfigDisable = bool(v != 0)
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Service", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowWasm
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthWasm
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthWasm
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Service = append(m.Service, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipWasm(dAtA[iNdEx:])

View File

@@ -114,6 +114,7 @@ message MatchRule {
repeated string domain = 2;
google.protobuf.Struct config = 3;
bool config_disable = 4;
repeated string service = 5;
}
// The phase in the filter chain where the plugin will be injected.

View File

@@ -64,6 +64,10 @@ spec:
items:
type: string
type: array
service:
items:
type: string
type: array
type: object
type: array
phase:

View File

@@ -22,6 +22,7 @@ import (
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/options"
"istio.io/istio/istioctl/pkg/writer/envoy/configdump"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
)
@@ -61,6 +62,23 @@ func NewDefaultGetEnvoyConfigOptions() *GetEnvoyConfigOptions {
}
}
func setupConfigdumpEnvoyConfigWriter(debug []byte, stdout io.Writer) (*configdump.ConfigWriter, error) {
cw := &configdump.ConfigWriter{Stdout: stdout}
err := cw.Prime(debug)
if err != nil {
return nil, err
}
return cw, nil
}
func GetEnvoyConfigWriter(config *GetEnvoyConfigOptions, stdout io.Writer) (*configdump.ConfigWriter, error) {
configDump, err := retrieveConfigDump(config.PodName, config.PodNamespace, config.BindAddress, config.IncludeEds)
if err != nil {
return nil, err
}
return setupConfigdumpEnvoyConfigWriter(configDump, stdout)
}
func GetEnvoyConfig(config *GetEnvoyConfigOptions) ([]byte, error) {
configDump, err := retrieveConfigDump(config.PodName, config.PodNamespace, config.BindAddress, config.IncludeEds)
if err != nil {
@@ -144,14 +162,12 @@ func formatGatewayConfig(configDump any, output string) ([]byte, error) {
if err != nil {
return nil, err
}
if output == "yaml" {
out, err = yaml.JSONToYAML(out)
if err != nil {
return nil, err
}
}
return out, nil
}

View File

@@ -0,0 +1,259 @@
diff --git a/source/common/router/BUILD b/source/common/router/BUILD
index 5c58501..4db76cd 100644
--- a/source/common/router/BUILD
+++ b/source/common/router/BUILD
@@ -212,6 +212,7 @@ envoy_cc_library(
"//envoy/router:rds_interface",
"//envoy/router:scopes_interface",
"//envoy/thread_local:thread_local_interface",
+ "//source/common/protobuf:utility_lib",
"@envoy_api//envoy/config/route/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto",
],
diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc
index ff7b4c8..5ac4523 100644
--- a/source/common/router/config_impl.cc
+++ b/source/common/router/config_impl.cc
@@ -550,19 +550,11 @@ RouteEntryImplBase::RouteEntryImplBase(const VirtualHostImpl& vhost,
"not be stripped: {}",
path_redirect_);
}
- ENVOY_LOG(info, "route stats is {}, name is {}", route.stat_prefix(), route.name());
if (!route.stat_prefix().empty()) {
route_stats_context_ = std::make_unique<RouteStatsContext>(
factory_context.scope(), factory_context.routerContext().routeStatNames(), vhost.statName(),
route.stat_prefix());
- } else if (!route.name().empty()) {
- // Added by Ingress
- // use route_name as default stat_prefix
- route_stats_context_ = std::make_unique<RouteStatsContext>(
- factory_context.scope(), factory_context.routerContext().routeStatNames(), vhost.statName(),
- route.name());
}
- // End Added
}
bool RouteEntryImplBase::evaluateRuntimeMatch(const uint64_t random_value) const {
@@ -1415,9 +1407,7 @@ VirtualHostImpl::VirtualHostImpl(
retry_shadow_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(
virtual_host, per_request_buffer_limit_bytes, std::numeric_limits<uint32_t>::max())),
include_attempt_count_in_request_(virtual_host.include_request_attempt_count()),
- include_attempt_count_in_response_(virtual_host.include_attempt_count_in_response()),
- virtual_cluster_catch_all_(*vcluster_scope_,
- factory_context.routerContext().virtualClusterStatNames()) {
+ include_attempt_count_in_response_(virtual_host.include_attempt_count_in_response()) {
switch (virtual_host.require_tls()) {
case envoy::config::route::v3::VirtualHost::NONE:
ssl_requirements_ = SslRequirements::None;
@@ -1478,10 +1468,14 @@ VirtualHostImpl::VirtualHostImpl(
}
}
- for (const auto& virtual_cluster : virtual_host.virtual_clusters()) {
- virtual_clusters_.push_back(
- VirtualClusterEntry(virtual_cluster, *vcluster_scope_,
- factory_context.routerContext().virtualClusterStatNames()));
+ if (!virtual_host.virtual_clusters().empty()) {
+ virtual_cluster_catch_all_ = std::make_unique<CatchAllVirtualCluster>(
+ *vcluster_scope_, factory_context.routerContext().virtualClusterStatNames());
+ for (const auto& virtual_cluster : virtual_host.virtual_clusters()) {
+ virtual_clusters_.push_back(
+ VirtualClusterEntry(virtual_cluster, *vcluster_scope_,
+ factory_context.routerContext().virtualClusterStatNames()));
+ }
}
if (virtual_host.has_cors()) {
@@ -1774,7 +1768,7 @@ VirtualHostImpl::virtualClusterFromEntries(const Http::HeaderMap& headers) const
}
if (!virtual_clusters_.empty()) {
- return &virtual_cluster_catch_all_;
+ return virtual_cluster_catch_all_.get();
}
return nullptr;
diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h
index cf0ddf3..d83eb94 100644
--- a/source/common/router/config_impl.h
+++ b/source/common/router/config_impl.h
@@ -352,10 +352,10 @@ private:
const bool include_attempt_count_in_response_;
absl::optional<envoy::config::route::v3::RetryPolicy> retry_policy_;
absl::optional<envoy::config::route::v3::HedgePolicy> hedge_policy_;
- const CatchAllVirtualCluster virtual_cluster_catch_all_;
#if defined(ALIMESH)
std::vector<std::string> allow_server_names_;
#endif
+ std::unique_ptr<const CatchAllVirtualCluster> virtual_cluster_catch_all_;
};
using VirtualHostSharedPtr = std::shared_ptr<VirtualHostImpl>;
diff --git a/source/common/router/scoped_config_impl.cc b/source/common/router/scoped_config_impl.cc
index 594d571..6482615 100644
--- a/source/common/router/scoped_config_impl.cc
+++ b/source/common/router/scoped_config_impl.cc
@@ -7,6 +7,8 @@
#include "source/common/http/header_utility.h"
#endif
+#include "source/common/protobuf/utility.h"
+
namespace Envoy {
namespace Router {
@@ -239,7 +241,8 @@ HeaderValueExtractorImpl::computeFragment(const Http::HeaderMap& headers) const
ScopedRouteInfo::ScopedRouteInfo(envoy::config::route::v3::ScopedRouteConfiguration&& config_proto,
ConfigConstSharedPtr&& route_config)
- : config_proto_(std::move(config_proto)), route_config_(std::move(route_config)) {
+ : config_proto_(std::move(config_proto)), route_config_(std::move(route_config)),
+ config_hash_(MessageUtil::hash(config_proto)) {
// TODO(stevenzzzz): Maybe worth a KeyBuilder abstraction when there are more than one type of
// Fragment.
for (const auto& fragment : config_proto_.key().fragments()) {
diff --git a/source/common/router/scoped_config_impl.h b/source/common/router/scoped_config_impl.h
index 9f6a1b2..28e2ee5 100644
--- a/source/common/router/scoped_config_impl.h
+++ b/source/common/router/scoped_config_impl.h
@@ -154,11 +154,13 @@ public:
return config_proto_;
}
const std::string& scopeName() const { return config_proto_.name(); }
+ uint64_t configHash() const { return config_hash_; }
private:
envoy::config::route::v3::ScopedRouteConfiguration config_proto_;
ScopeKey scope_key_;
ConfigConstSharedPtr route_config_;
+ const uint64_t config_hash_;
};
using ScopedRouteInfoConstSharedPtr = std::shared_ptr<const ScopedRouteInfo>;
// Ordered map for consistent config dumping.
diff --git a/source/common/router/scoped_rds.cc b/source/common/router/scoped_rds.cc
index 133e91e..9b2096e 100644
--- a/source/common/router/scoped_rds.cc
+++ b/source/common/router/scoped_rds.cc
@@ -245,6 +245,11 @@ bool ScopedRdsConfigSubscription::addOrUpdateScopes(
dynamic_cast<const envoy::config::route::v3::ScopedRouteConfiguration&>(
resource.get().resource());
const std::string scope_name = scoped_route_config.name();
+ if (const auto& scope_info_iter = scoped_route_map_.find(scope_name);
+ scope_info_iter != scoped_route_map_.end() &&
+ scope_info_iter->second->configHash() == MessageUtil::hash(scoped_route_config)) {
+ continue;
+ }
rds.set_route_config_name(scoped_route_config.route_configuration_name());
std::unique_ptr<RdsRouteConfigProviderHelper> rds_config_provider_helper;
std::shared_ptr<ScopedRouteInfo> scoped_route_info = nullptr;
@@ -398,6 +403,7 @@ void ScopedRdsConfigSubscription::onRdsConfigUpdate(const std::string& scope_nam
auto new_scoped_route_info = std::make_shared<ScopedRouteInfo>(
envoy::config::route::v3::ScopedRouteConfiguration(iter->second->configProto()),
std::move(new_rds_config));
+ scoped_route_map_[new_scoped_route_info->scopeName()] = new_scoped_route_info;
applyConfigUpdate([new_scoped_route_info](ConfigProvider::ConfigConstSharedPtr config)
-> ConfigProvider::ConfigConstSharedPtr {
auto* thread_local_scoped_config =
diff --git a/source/common/router/scoped_rds.h b/source/common/router/scoped_rds.h
index d21d812..a510c1f 100644
--- a/source/common/router/scoped_rds.h
+++ b/source/common/router/scoped_rds.h
@@ -104,7 +104,7 @@ struct ScopedRdsStats {
// A scoped RDS subscription to be used with the dynamic scoped RDS ConfigProvider.
class ScopedRdsConfigSubscription
: public Envoy::Config::DeltaConfigSubscriptionInstance,
- Envoy::Config::SubscriptionBase<envoy::config::route::v3::ScopedRouteConfiguration> {
+ public Envoy::Config::SubscriptionBase<envoy::config::route::v3::ScopedRouteConfiguration> {
public:
using ScopedRouteConfigurationMap =
std::map<std::string, envoy::config::route::v3::ScopedRouteConfiguration>;
diff --git a/test/common/router/scoped_config_impl_test.cc b/test/common/router/scoped_config_impl_test.cc
index f63f258..69a2f4b 100644
--- a/test/common/router/scoped_config_impl_test.cc
+++ b/test/common/router/scoped_config_impl_test.cc
@@ -452,6 +452,24 @@ TEST_F(ScopedRouteInfoTest, Creation) {
EXPECT_EQ(info_->scopeKey(), makeKey({"foo", "bar"}));
}
+// Tests that config hash changes if ScopedRouteConfiguration of the ScopedRouteInfo changes.
+TEST_F(ScopedRouteInfoTest, Hash) {
+ const envoy::config::route::v3::ScopedRouteConfiguration config_copy = scoped_route_config_;
+ info_ = std::make_unique<ScopedRouteInfo>(scoped_route_config_, route_config_);
+ EXPECT_EQ(info_->routeConfig().get(), route_config_.get());
+ EXPECT_TRUE(TestUtility::protoEqual(info_->configProto(), config_copy));
+ EXPECT_EQ(info_->scopeName(), "foo_scope");
+ EXPECT_EQ(info_->scopeKey(), makeKey({"foo", "bar"}));
+
+ const auto info2 = std::make_unique<ScopedRouteInfo>(scoped_route_config_, route_config_);
+ ASSERT_EQ(info2->configHash(), info_->configHash());
+
+ // Mutate the config and hash should be different now.
+ scoped_route_config_.set_on_demand(true);
+ const auto info3 = std::make_unique<ScopedRouteInfo>(scoped_route_config_, route_config_);
+ ASSERT_NE(info3->configHash(), info_->configHash());
+}
+
class ScopedConfigImplTest : public testing::Test {
public:
void SetUp() override {
diff --git a/test/common/router/scoped_rds_test.cc b/test/common/router/scoped_rds_test.cc
index 09b96a6..b4776c9 100644
--- a/test/common/router/scoped_rds_test.cc
+++ b/test/common/router/scoped_rds_test.cc
@@ -13,6 +13,7 @@
#include "envoy/stats/scope.h"
#include "source/common/config/api_version.h"
+#include "source/common/config/config_provider_impl.h"
#include "source/common/config/grpc_mux_impl.h"
#include "source/common/protobuf/message_validator_impl.h"
#include "source/common/router/scoped_rds.h"
@@ -365,6 +366,48 @@ key:
"Didn't find a registered implementation for name: 'filter.unknown'");
}
+// Test that scopes with same config as existing scopes will be skipped in a config push.
+TEST_F(ScopedRdsTest, UnchangedScopesAreSkipped) {
+ setup();
+ init_watcher_.expectReady();
+ const std::string config_yaml = R"EOF(
+name: foo_scope
+route_configuration_name: foo_routes
+key:
+ fragments:
+ - string_key: x-foo-key
+)EOF";
+ const auto resource = parseScopedRouteConfigurationFromYaml(config_yaml);
+ const std::string config_yaml2 = R"EOF(
+name: foo_scope2
+route_configuration_name: foo_routes
+key:
+ fragments:
+ - string_key: x-bar-key
+)EOF";
+ const auto resource_2 = parseScopedRouteConfigurationFromYaml(config_yaml2);
+
+ // Delta API.
+ const auto decoded_resources = TestUtility::decodeResources({resource, resource_2});
+ context_init_manager_.initialize(init_watcher_);
+ EXPECT_NO_THROW(srds_subscription_->onConfigUpdate(decoded_resources.refvec_, {}, "v1"));
+ EXPECT_EQ(1UL,
+ server_factory_context_.scope_.counter("foo.scoped_rds.foo_scoped_routes.config_reload")
+ .value());
+ EXPECT_EQ(2UL, all_scopes_.value());
+ pushRdsConfig({"foo_routes"}, "111");
+ Envoy::Router::ScopedRdsConfigSubscription* srds_delta_subscription =
+ static_cast<Envoy::Router::ScopedRdsConfigSubscription*>(srds_subscription_);
+ ASSERT_NE(srds_delta_subscription, nullptr);
+ ASSERT_EQ("v1", srds_delta_subscription->configInfo()->last_config_version_);
+ // Push again the same set of config with different version number, the config will be skipped.
+ EXPECT_NO_THROW(srds_subscription_->onConfigUpdate(decoded_resources.refvec_, {}, "123"));
+ ASSERT_EQ("v1", srds_delta_subscription->configInfo()->last_config_version_);
+ EXPECT_EQ(2UL,
+ server_factory_context_.scope_.counter("foo.scoped_rds.foo_scoped_routes.config_reload")
+ .value());
+}
+
// Test ignoring the optional unknown factory in the per-virtualhost typed config.
TEST_F(ScopedRdsTest, OptionalUnknownFactoryForPerVirtualHostTypedConfig) {
OptionalHttpFilters optional_http_filters;

View File

@@ -0,0 +1,13 @@
diff --git a/source/common/http/headers.h b/source/common/http/headers.h
index a7a8a3393e..6af4a2852d 100644
--- a/source/common/http/headers.h
+++ b/source/common/http/headers.h
@@ -123,7 +123,7 @@ public:
const LowerCaseString TriCostTime{"req-cost-time"};
const LowerCaseString TriStartTime{"req-start-time"};
const LowerCaseString TriRespStartTime{"resp-start-time"};
- const LowerCaseString EnvoyOriginalHost{"original-host"};
+ const LowerCaseString EnvoyOriginalHost{"x-envoy-original-host"};
const LowerCaseString HigressOriginalService{"x-higress-original-service"};
} AliExtendedValues;
#endif

View File

@@ -0,0 +1,43 @@
diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc
index 9642d8abd3..410baa856f 100644
--- a/source/extensions/common/wasm/context.cc
+++ b/source/extensions/common/wasm/context.cc
@@ -62,6 +62,21 @@ constexpr absl::string_view CelStateKeyPrefix = "wasm.";
#if defined(ALIMESH)
constexpr std::string_view ClearRouteCacheKey = "clear_route_cache";
constexpr std::string_view DisableClearRouteCache = "off";
+constexpr std::string_view SetDecoderBufferLimit = "set_decoder_buffer_limit";
+constexpr std::string_view SetEncoderBufferLimit = "set_encoder_buffer_limit";
+
+bool stringViewToUint32(std::string_view str, uint32_t& out_value) {
+ try {
+ unsigned long temp = std::stoul(std::string(str));
+ if (temp <= std::numeric_limits<uint32_t>::max()) {
+ out_value = static_cast<uint32_t>(temp);
+ return true;
+ }
+ } catch (const std::exception& e) {
+ ENVOY_LOG_MISC(critical, "stringToUint exception '{}'", e.what());
+ }
+ return false;
+}
#endif
using HashPolicy = envoy::config::route::v3::RouteAction::HashPolicy;
@@ -1280,6 +1295,16 @@ WasmResult Context::setProperty(std::string_view path, std::string_view value) {
} else {
disable_clear_route_cache_ = false;
}
+ } else if (path == SetDecoderBufferLimit && decoder_callbacks_) {
+ uint32_t buffer_limit;
+ if (stringViewToUint32(value, buffer_limit)) {
+ decoder_callbacks_->setDecoderBufferLimit(buffer_limit);
+ }
+ } else if (path == SetEncoderBufferLimit && encoder_callbacks_) {
+ uint32_t buffer_limit;
+ if (stringViewToUint32(value, buffer_limit)) {
+ encoder_callbacks_->setEncoderBufferLimit(buffer_limit);
+ }
}
#endif
if (!state->setValue(toAbslStringView(value))) {

View File

@@ -0,0 +1,106 @@
diff --git a/envoy/stream_info/stream_info.h b/envoy/stream_info/stream_info.h
index c6d82db4f4..09717673b0 100644
--- a/envoy/stream_info/stream_info.h
+++ b/envoy/stream_info/stream_info.h
@@ -613,7 +613,21 @@ public:
* @return the number of times the request was attempted upstream, absl::nullopt if the request
* was never attempted upstream.
*/
+
virtual absl::optional<uint32_t> attemptCount() const PURE;
+
+#ifdef ALIMESH
+ /**
+ * @param key the filter state key set by wasm filter.
+ * @param value the filter state value set by wasm filter.
+ */
+ virtual void setCustomSpanTag(const std::string& key, const std::string& value) PURE;
+
+ /**
+ * @return the key-value map of filter states set by wasm filter.
+ */
+ virtual const std::unordered_map<std::string, std::string>& getCustomSpanTagMap() const PURE;
+#endif
};
} // namespace StreamInfo
diff --git a/source/common/stream_info/stream_info_impl.h b/source/common/stream_info/stream_info_impl.h
index 6ce2afe773..d5e7a80b37 100644
--- a/source/common/stream_info/stream_info_impl.h
+++ b/source/common/stream_info/stream_info_impl.h
@@ -291,6 +291,20 @@ struct StreamInfoImpl : public StreamInfo {
absl::optional<uint32_t> attemptCount() const override { return attempt_count_; }
+#ifdef ALIMESH
+ void setCustomSpanTag(const std::string& key, const std::string& value) override {
+ auto it = custom_span_tags_.find(key);
+ if (it != custom_span_tags_.end()) {
+ it->second = value;
+ } else {
+ custom_span_tags_.emplace(key, value);
+ }
+ }
+
+ const std::unordered_map<std::string, std::string>& getCustomSpanTagMap() const override {
+ return custom_span_tags_;
+ }
+#endif
TimeSource& time_source_;
const SystemTime start_time_;
const MonotonicTime start_time_monotonic_;
@@ -350,6 +364,9 @@ private:
absl::optional<Upstream::ClusterInfoConstSharedPtr> upstream_cluster_info_;
std::string filter_chain_name_;
Tracing::Reason trace_reason_;
+#ifdef ALIMESH
+ std::unordered_map<std::string, std::string> custom_span_tags_;
+#endif
};
} // namespace StreamInfo
diff --git a/source/common/tracing/http_tracer_impl.cc b/source/common/tracing/http_tracer_impl.cc
index e55cf00e0a..f94e9101d7 100644
--- a/source/common/tracing/http_tracer_impl.cc
+++ b/source/common/tracing/http_tracer_impl.cc
@@ -214,6 +214,14 @@ void HttpTracerUtility::setCommonTags(Span& span, const Http::ResponseHeaderMap*
span.setTag(Tracing::Tags::get().Component, Tracing::Tags::get().Proxy);
+#ifdef ALIMESH
+ // Wasm filter state
+ const auto& custom_span_tags = stream_info.getCustomSpanTagMap();
+ for (const auto& it : custom_span_tags) {
+ span.setTag(it.first, it.second);
+ }
+#endif
+
if (nullptr != stream_info.upstreamHost()) {
span.setTag(Tracing::Tags::get().UpstreamCluster, stream_info.upstreamHost()->cluster().name());
span.setTag(Tracing::Tags::get().UpstreamClusterName,
diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc
index 410baa856f..b11ecf1cd6 100644
--- a/source/extensions/common/wasm/context.cc
+++ b/source/extensions/common/wasm/context.cc
@@ -60,6 +60,7 @@ namespace {
constexpr absl::string_view CelStateKeyPrefix = "wasm.";
#if defined(ALIMESH)
+constexpr absl::string_view CustomeTraceSpanTagPrefix = "trace_span_tag.";
constexpr std::string_view ClearRouteCacheKey = "clear_route_cache";
constexpr std::string_view DisableClearRouteCache = "off";
constexpr std::string_view SetDecoderBufferLimit = "set_decoder_buffer_limit";
@@ -1271,6 +1272,13 @@ WasmResult Context::setProperty(std::string_view path, std::string_view value) {
if (!stream_info) {
return WasmResult::NotFound;
}
+#ifdef ALIMESH
+ if (absl::StartsWith(absl::string_view{path.data(), path.size()}, CustomeTraceSpanTagPrefix)) {
+ stream_info->setCustomSpanTag(std::string(path.substr(CustomeTraceSpanTagPrefix.size())),
+ std::string(value));
+ return WasmResult::Ok;
+ }
+#endif
std::string key;
absl::StrAppend(&key, CelStateKeyPrefix, toAbslStringView(path));
CelState* state;

341
get_helm.sh Executable file
View File

@@ -0,0 +1,341 @@
#!/usr/bin/env bash
# Copyright The Helm Authors.
#
# 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.
# The install script is based off of the MIT-licensed script from glide,
# the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get
: ${BINARY_NAME:="helm"}
: ${USE_SUDO:="true"}
: ${DEBUG:="false"}
: ${VERIFY_CHECKSUM:="true"}
: ${VERIFY_SIGNATURES:="false"}
: ${HELM_INSTALL_DIR:="/usr/local/bin"}
: ${GPG_PUBRING:="pubring.kbx"}
HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)"
HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)"
HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)"
HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)"
HAS_GIT="$(type "git" &> /dev/null && echo true || echo false)"
# initArch discovers the architecture for this system.
initArch() {
ARCH=$(uname -m)
case $ARCH in
armv5*) ARCH="armv5";;
armv6*) ARCH="armv6";;
armv7*) ARCH="arm";;
aarch64) ARCH="arm64";;
x86) ARCH="386";;
x86_64) ARCH="amd64";;
i686) ARCH="386";;
i386) ARCH="386";;
esac
}
# initOS discovers the operating system for this system.
initOS() {
OS=$(echo `uname`|tr '[:upper:]' '[:lower:]')
case "$OS" in
# Minimalist GNU for Windows
mingw*|cygwin*) OS='windows';;
esac
}
# runs the given command as root (detects if we are root already)
runAsRoot() {
if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then
sudo "${@}"
else
"${@}"
fi
}
# verifySupported checks that the os/arch combination is supported for
# binary builds, as well whether or not necessary tools are present.
verifySupported() {
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
echo "No prebuilt binary for ${OS}-${ARCH}."
echo "To build from source, go to https://github.com/helm/helm"
exit 1
fi
if [ "${HAS_CURL}" != "true" ] && [ "${HAS_WGET}" != "true" ]; then
echo "Either curl or wget is required"
exit 1
fi
if [ "${VERIFY_CHECKSUM}" == "true" ] && [ "${HAS_OPENSSL}" != "true" ]; then
echo "In order to verify checksum, openssl must first be installed."
echo "Please install openssl or set VERIFY_CHECKSUM=false in your environment."
exit 1
fi
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
if [ "${HAS_GPG}" != "true" ]; then
echo "In order to verify signatures, gpg must first be installed."
echo "Please install gpg or set VERIFY_SIGNATURES=false in your environment."
exit 1
fi
if [ "${OS}" != "linux" ]; then
echo "Signature verification is currently only supported on Linux."
echo "Please set VERIFY_SIGNATURES=false or verify the signatures manually."
exit 1
fi
fi
if [ "${HAS_GIT}" != "true" ]; then
echo "[WARNING] Could not find git. It is required for plugin installation."
fi
}
# checkDesiredVersion checks if the desired version is available.
checkDesiredVersion() {
if [ "x$DESIRED_VERSION" == "x" ]; then
# Get tag from release URL
local latest_release_url="https://get.helm.sh/helm-latest-version"
local latest_release_response=""
if [ "${HAS_CURL}" == "true" ]; then
latest_release_response=$( curl -L --silent --show-error --fail "$latest_release_url" 2>&1 || true )
elif [ "${HAS_WGET}" == "true" ]; then
latest_release_response=$( wget "$latest_release_url" -q -O - 2>&1 || true )
fi
TAG=$( echo "$latest_release_response" | grep '^v[0-9]' )
if [ "x$TAG" == "x" ]; then
printf "Could not retrieve the latest release tag information from %s: %s\n" "${latest_release_url}" "${latest_release_response}"
exit 1
fi
else
TAG=$DESIRED_VERSION
fi
}
# checkHelmInstalledVersion checks which version of helm is installed and
# if it needs to be changed.
checkHelmInstalledVersion() {
if [[ -f "${HELM_INSTALL_DIR}/${BINARY_NAME}" ]]; then
local version=$("${HELM_INSTALL_DIR}/${BINARY_NAME}" version --template="{{ .Version }}")
if [[ "$version" == "$TAG" ]]; then
echo "Helm ${version} is already ${DESIRED_VERSION:-latest}"
return 0
else
echo "Helm ${TAG} is available. Changing from version ${version}."
return 1
fi
else
return 1
fi
}
# downloadFile downloads the latest binary package and also the checksum
# for that binary.
downloadFile() {
HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz"
DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST"
CHECKSUM_URL="$DOWNLOAD_URL.sha256"
HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)"
HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST"
HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256"
echo "Downloading $DOWNLOAD_URL"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE"
curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL"
wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL"
fi
}
# verifyFile verifies the SHA256 checksum of the binary package
# and the GPG signatures for both the package and checksum file
# (depending on settings in environment).
verifyFile() {
if [ "${VERIFY_CHECKSUM}" == "true" ]; then
verifyChecksum
fi
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
verifySignatures
fi
}
# installFile installs the Helm binary.
installFile() {
HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME"
mkdir -p "$HELM_TMP"
tar xf "$HELM_TMP_FILE" -C "$HELM_TMP"
HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm"
echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}"
runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME"
echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME"
}
# verifyChecksum verifies the SHA256 checksum of the binary package.
verifyChecksum() {
printf "Verifying checksum... "
local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}')
local expected_sum=$(cat ${HELM_SUM_FILE})
if [ "$sum" != "$expected_sum" ]; then
echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting."
exit 1
fi
echo "Done."
}
# verifySignatures obtains the latest KEYS file from GitHub main branch
# as well as the signature .asc files from the specific GitHub release,
# then verifies that the release artifacts were signed by a maintainer's key.
verifySignatures() {
printf "Verifying signatures... "
local keys_filename="KEYS"
local github_keys_url="https://raw.githubusercontent.com/helm/helm/main/${keys_filename}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_keys_url}" -o "${HELM_TMP_ROOT}/${keys_filename}"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "${HELM_TMP_ROOT}/${keys_filename}" "${github_keys_url}"
fi
local gpg_keyring="${HELM_TMP_ROOT}/keyring.gpg"
local gpg_homedir="${HELM_TMP_ROOT}/gnupg"
mkdir -p -m 0700 "${gpg_homedir}"
local gpg_stderr_device="/dev/null"
if [ "${DEBUG}" == "true" ]; then
gpg_stderr_device="/dev/stderr"
fi
gpg --batch --quiet --homedir="${gpg_homedir}" --import "${HELM_TMP_ROOT}/${keys_filename}" 2> "${gpg_stderr_device}"
gpg --batch --no-default-keyring --keyring "${gpg_homedir}/${GPG_PUBRING}" --export > "${gpg_keyring}"
local github_release_url="https://github.com/helm/helm/releases/download/${TAG}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
fi
local error_text="If you think this might be a potential security issue,"
error_text="${error_text}\nplease see here: https://github.com/helm/community/blob/master/SECURITY.md"
local num_goodlines_sha=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
if [[ ${num_goodlines_sha} -lt 2 ]]; then
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256!"
echo -e "${error_text}"
exit 1
fi
local num_goodlines_tar=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
if [[ ${num_goodlines_tar} -lt 2 ]]; then
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz!"
echo -e "${error_text}"
exit 1
fi
echo "Done."
}
# fail_trap is executed if an error occurs.
fail_trap() {
result=$?
if [ "$result" != "0" ]; then
if [[ -n "$INPUT_ARGUMENTS" ]]; then
echo "Failed to install $BINARY_NAME with the arguments provided: $INPUT_ARGUMENTS"
help
else
echo "Failed to install $BINARY_NAME"
fi
echo -e "\tFor support, go to https://github.com/helm/helm."
fi
cleanup
exit $result
}
# testVersion tests the installed client to make sure it is working.
testVersion() {
set +e
HELM="$(command -v $BINARY_NAME)"
if [ "$?" = "1" ]; then
echo "$BINARY_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?'
exit 1
fi
set -e
}
# help provides possible cli installation arguments
help () {
echo "Accepted cli arguments are:"
echo -e "\t[--help|-h ] ->> prints this help"
echo -e "\t[--version|-v <desired_version>] . When not defined it fetches the latest release from GitHub"
echo -e "\te.g. --version v3.0.0 or -v canary"
echo -e "\t[--no-sudo] ->> install without sudo"
}
# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977
cleanup() {
if [[ -d "${HELM_TMP_ROOT:-}" ]]; then
rm -rf "$HELM_TMP_ROOT"
fi
}
# Execution
#Stop execution on any error
trap "fail_trap" EXIT
set -e
# Set debug if desired
if [ "${DEBUG}" == "true" ]; then
set -x
fi
# Parsing input arguments (if any)
export INPUT_ARGUMENTS="${@}"
set -u
while [[ $# -gt 0 ]]; do
case $1 in
'--version'|-v)
shift
if [[ $# -ne 0 ]]; then
export DESIRED_VERSION="${1}"
if [[ "$1" != "v"* ]]; then
echo "Expected version arg ('${DESIRED_VERSION}') to begin with 'v', fixing..."
export DESIRED_VERSION="v${1}"
fi
else
echo -e "Please provide the desired version. e.g. --version v3.0.0 or -v canary"
exit 0
fi
;;
'--no-sudo')
USE_SUDO="false"
;;
'--help'|-h)
help
exit 0
;;
*) exit 1
;;
esac
shift
done
set +u
initArch
initOS
verifySupported
checkDesiredVersion
if ! checkHelmInstalledVersion; then
downloadFile
verifyFile
installFile
fi
testVersion
cleanup

10
go.mod
View File

@@ -255,11 +255,9 @@ require (
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.15.0 // indirect
@@ -281,6 +279,8 @@ require (
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/apiserver v0.22.5 // indirect
k8s.io/component-base v0.22.5 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
oras.land/oras-go v0.4.0 // indirect
@@ -303,7 +303,7 @@ replace istio.io/client-go => ./external/client-go
replace istio.io/istio => ./external/istio
replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0.1
replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0.2
require (
github.com/caddyserver/certmagic v0.20.0
@@ -312,10 +312,10 @@ require (
github.com/kylelemons/godebug v1.1.0
github.com/mholt/acmez v1.2.0
github.com/tidwall/gjson v1.17.0
go.uber.org/zap v1.24.0
golang.org/x/net v0.17.0
helm.sh/helm/v3 v3.7.1
k8s.io/apiextensions-apiserver v0.25.4
k8s.io/component-base v0.22.5
k8s.io/klog/v2 v2.60.1
knative.dev/networking v0.0.0-20220302134042-e8b2eb995165
knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
)

4
go.sum
View File

@@ -61,8 +61,8 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/2456868764/certmagic v1.0.1 h1:dRzow2Npe9llFTBhNVl0fVe8Yi/Q14ygNonlaZUyDZQ=
github.com/2456868764/certmagic v1.0.1/go.mod h1:LOn81EQYMPajdew6Ln6SVdHPxPqPv6jwsUg92kiNlcQ=
github.com/2456868764/certmagic v1.0.2 h1:xYoN4z6seONwT85llWXZcASvQME8TOSiSWQvLJsGGsE=
github.com/2456868764/certmagic v1.0.2/go.mod h1:LOn81EQYMPajdew6Ln6SVdHPxPqPv6jwsUg92kiNlcQ=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210929163055-e81b3f25be97/go.mod h1:WpB7kf89yJUETZxQnP1kgYPNwlT2jjdDYUCoxVggM3g=
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.4.0
appVersion: 1.4.2
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -10,4 +10,4 @@ name: higress-core
sources:
- http://github.com/alibaba/higress
type: application
version: 1.4.0
version: 1.4.2

View File

@@ -97,7 +97,7 @@ higress: {{ include "controller.name" . }}
{{- end }}
{{- define "skywalking.enabled" -}}
{{- if and .Values.skywalking.enabled .Values.skywalking.service.address }}
{{- if and (hasKey .Values "tracing") .Values.tracing.enable (hasKey .Values.tracing "skywalking") .Values.tracing.skywalking.service }}
true
{{- end }}
{{- end }}

View File

@@ -46,10 +46,6 @@
address: {{ .Values.global.tracer.lightstep.address }}
# Access Token used to communicate with the Satellite pool
accessToken: {{ .Values.global.tracer.lightstep.accessToken }}
{{- else if eq .Values.global.proxy.tracer "zipkin" }}
zipkin:
# Address of the Zipkin collector
address: {{ .Values.global.tracer.zipkin.address | default (print "zipkin." .Release.Namespace ":9411") }}
{{- else if eq .Values.global.proxy.tracer "datadog" }}
datadog:
# Address of the Datadog Agent
@@ -88,7 +84,7 @@
{{- if .Values.global.enableHigressIstio }}
discoveryAddress: {{ printf "istiod.%s.svc" .Values.global.istioNamespace }}:15012
{{- else }}
discoveryAddress: higress-controller.{{.Release.Namespace}}.svc:15012
discoveryAddress: {{ include "controller.name" . }}.{{.Release.Namespace}}.svc:15012
{{- end }}
{{- end }}
proxyStatsMatcher:
@@ -109,7 +105,17 @@ metadata:
labels:
{{- include "gateway.labels" . | nindent 4 }}
data:
higress: |-
{{- $existingConfig := lookup "v1" "ConfigMap" .Release.Namespace "higress-config" }}
{{- $existingData := dict }}
{{- if $existingConfig }}
{{- $existingData = index $existingConfig.data "higress" | default "{}" | fromYaml }}
{{- end }}
{{- $newData := dict }}
{{- if and (hasKey .Values "tracing") .Values.tracing.enable }}
{{- $_ := set $newData "tracing" .Values.tracing }}
{{- end }}
{{- toYaml (merge $existingData $newData) | nindent 4 }}
# Configuration file for the mesh networks to be used by the Split Horizon EDS.
meshNetworks: |-
{{- if .Values.global.meshNetworks }}
@@ -170,8 +176,8 @@ data:
"endpoint": {
"address": {
"socket_address": {
"address": "{{ .Values.skywalking.service.address }}",
"port_value": "{{ .Values.skywalking.service.port }}"
"address": "{{ .Values.tracing.skywalking.service }}",
"port_value": "{{ .Values.tracing.skywalking.port }}"
}
}
}

View File

@@ -9,7 +9,7 @@ rules:
# ingress controller
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
verbs: ["create", "get", "list", "watch", "update", "delete", "patch"]
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["*"]
@@ -36,7 +36,7 @@ rules:
# Needed for multicluster secret reading, possibly ingress certs in the future
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
- apiGroups: ["networking.higress.io"]
resources: ["mcpbridges"]
@@ -61,12 +61,12 @@ rules:
# discovery and routing
- apiGroups: [""]
resources: ["pods", "nodes", "services", "namespaces", "endpoints"]
resources: ["pods", "nodes", "services", "namespaces", "endpoints", "deployments"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
# Istiod and bootstrap.
- apiGroups: ["certificates.k8s.io"]
resources:
@@ -100,7 +100,7 @@ rules:
- apiGroups: ["multicluster.x-k8s.io"]
resources: ["serviceimports"]
verbs: ["get", "watch", "list"]
# sidecar injection controller
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["mutatingwebhookconfigurations"]

View File

@@ -26,9 +26,70 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "controller.serviceAccountName" . }}
{{- if .Values.global.priorityClassName }}
priorityClassName: "{{ .Values.global.priorityClassName }}"
{{- end }}
securityContext:
{{- toYaml .Values.controller.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.controller.securityContext | nindent 12 }}
image: "{{ .Values.controller.hub | default .Values.global.hub }}/{{ .Values.controller.image | default "higress" }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
args:
- "serve"
- --gatewaySelectorKey=higress
- --gatewaySelectorValue={{ .Release.Namespace }}-{{ include "gateway.name" . }}
- --gatewayHttpPort={{ .Values.gateway.httpPort }}
- --gatewayHttpsPort={{ .Values.gateway.httpsPort }}
{{- if not .Values.global.enableStatus }}
- --enableStatus={{ .Values.global.enableStatus }}
{{- end }}
- --ingressClass={{ .Values.global.ingressClass }}
{{- if .Values.global.watchNamespace }}
- --watchNamespace={{ .Values.global.watchNamespace }}
{{- end }}
- --enableAutomaticHttps={{ .Values.controller.automaticHttps.enabled }}
- --automaticHttpsEmail={{ .Values.controller.automaticHttps.email }}
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: DOMAIN_SUFFIX
value: {{ .Values.global.proxy.clusterDomain }}
{{- if .Values.controller.env }}
{{- range $key, $val := .Values.controller.env }}
- name: {{ $key }}
value: "{{ $val }}"
{{- end }}
{{- end }}
ports:
{{- range $idx, $port := .Values.controller.ports }}
- name: {{ $port.name }}
containerPort: {{ $port.port }}
protocol: {{ $port.protocol }}
{{- end }}
readinessProbe:
{{- toYaml .Values.controller.probe | nindent 12 }}
{{- if not (or .Values.global.local .Values.global.kind) }}
resources:
{{- toYaml .Values.controller.resources | nindent 12 }}
{{- end }}
volumeMounts:
- name: log
mountPath: /var/log
{{- if not .Values.global.enableHigressIstio }}
- name: discovery
image: "{{ .Values.pilot.hub | default .Values.global.hub }}/{{ .Values.pilot.image | default "pilot" }}:{{ .Values.pilot.tag | default .Chart.AppVersion }}"
@@ -191,64 +252,6 @@ spec:
mountPath: /cacerts
{{- end }}
{{- end }}
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.controller.securityContext | nindent 12 }}
image: "{{ .Values.controller.hub | default .Values.global.hub }}/{{ .Values.controller.image | default "higress" }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
args:
- "serve"
- --gatewaySelectorKey=higress
- --gatewaySelectorValue={{ .Release.Namespace }}-{{ include "gateway.name" . }}
- --gatewayHttpPort={{ .Values.gateway.httpPort }}
- --gatewayHttpsPort={{ .Values.gateway.httpsPort }}
{{- if not .Values.global.enableStatus }}
- --enableStatus={{ .Values.global.enableStatus }}
{{- end }}
- --ingressClass={{ .Values.global.ingressClass }}
{{- if .Values.global.watchNamespace }}
- --watchNamespace={{ .Values.global.watchNamespace }}
{{- end }}
- --enableAutomaticHttps={{ .Values.controller.automaticHttps.enabled }}
- --automaticHttpsEmail={{ .Values.controller.automaticHttps.email }}
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: DOMAIN_SUFFIX
value: {{ .Values.global.proxy.clusterDomain }}
{{- if .Values.controller.env }}
{{- range $key, $val := .Values.controller.env }}
- name: {{ $key }}
value: "{{ $val }}"
{{- end }}
{{- end }}
ports:
{{- range $idx, $port := .Values.controller.ports }}
- name: {{ $port.name }}
containerPort: {{ $port.port }}
protocol: {{ $port.protocol }}
{{- end }}
readinessProbe:
{{- toYaml .Values.controller.probe | nindent 12 }}
{{- if not (or .Values.global.local .Values.global.kind) }}
resources:
{{- toYaml .Values.controller.resources | nindent 12 }}
{{- end }}
volumeMounts:
- name: log
mountPath: /var/log
{{- with .Values.controller.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View File

@@ -0,0 +1,332 @@
{{- if eq .Values.gateway.kind "DaemonSet" -}}
{{- $o11y := .Values.global.o11y }}
{{- $unprivilegedPortSupported := true }}
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
{{- $kernelVersion := $node.status.nodeInfo.kernelVersion }}
{{- if $kernelVersion }}
{{- $kernelVersion = regexFind "^(\\d+\\.\\d+\\.\\d+)" $kernelVersion }}
{{- if and $kernelVersion (semverCompare "<4.11.0" $kernelVersion) }}
{{- $unprivilegedPortSupported = false }}
{{- end }}
{{- end }}
{{- end -}}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "gateway.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "gateway.labels" . | nindent 4}}
annotations:
{{- .Values.gateway.annotations | toYaml | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "gateway.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- if .Values.global.enableHigressIstio }}
"enableHigressIstio": "true"
{{- end }}
{{- if .Values.gateway.podAnnotations }}
{{- toYaml .Values.gateway.podAnnotations | nindent 8 }}
{{- end }}
labels:
sidecar.istio.io/inject: "false"
{{- with .Values.gateway.revision }}
istio.io/rev: {{ . }}
{{- end }}
{{- include "gateway.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.gateway.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "gateway.serviceAccountName" . }}
{{- if .Values.global.priorityClassName }}
priorityClassName: "{{ .Values.global.priorityClassName }}"
{{- end }}
securityContext:
{{- if .Values.gateway.securityContext }}
{{- toYaml .Values.gateway.securityContext | nindent 8 }}
{{- else if and $unprivilegedPortSupported (and (not .Values.gateway.hostNetwork) (semverCompare ">=1.22-0" .Capabilities.KubeVersion.GitVersion)) }}
# Safe since 1.22: https://github.com/kubernetes/kubernetes/pull/103326
sysctls:
- name: net.ipv4.ip_unprivileged_port_start
value: "0"
{{- end }}
containers:
{{- if $o11y.enabled }}
{{- $config := $o11y.promtail }}
- name: promtail
image: {{ $config.image.repository }}:{{ $config.image.tag }}
imagePullPolicy: IfNotPresent
args:
- -config.file=/etc/promtail/promtail.yaml
env:
- name: 'HOSTNAME'
valueFrom:
fieldRef:
fieldPath: 'spec.nodeName'
ports:
- containerPort: {{ $config.port }}
name: http-metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: {{ $config.port }}
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
volumeMounts:
- name: promtail-config
mountPath: "/etc/promtail"
- name: log
mountPath: /var/log/proxy
- name: tmp
mountPath: /tmp
{{- end }}
- name: higress-gateway
image: "{{ .Values.gateway.hub | default .Values.global.hub }}/{{ .Values.gateway.image | default "gateway" }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
args:
- proxy
- router
- --domain
- $(POD_NAMESPACE).svc.cluster.local
- --proxyLogLevel=warning
- --proxyComponentLogLevel=misc:error
- --log_output_level=all:info
- --serviceCluster=higress-gateway
securityContext:
{{- if .Values.gateway.containerSecurityContext }}
{{- toYaml .Values.gateway.containerSecurityContext | nindent 12 }}
{{- else if and $unprivilegedPortSupported (and (not .Values.gateway.hostNetwork) (semverCompare ">=1.22-0" .Capabilities.KubeVersion.GitVersion)) }}
# Safe since 1.22: https://github.com/kubernetes/kubernetes/pull/103326
capabilities:
drop:
- ALL
allowPrivilegeEscalation: false
privileged: false
# When enabling lite metrics, the configuration template files need to be replaced.
{{- if not .Values.global.liteMetrics }}
readOnlyRootFilesystem: true
{{- end }}
runAsUser: 1337
runAsGroup: 1337
runAsNonRoot: true
{{- else }}
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
runAsUser: 0
runAsGroup: 1337
runAsNonRoot: false
allowPrivilegeEscalation: true
{{- end }}
env:
- name: NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: INSTANCE_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
- name: PILOT_XDS_SEND_TIMEOUT
value: 60s
- name: PROXY_XDS_VIA_AGENT
value: "true"
- name: ENABLE_INGRESS_GATEWAY_SDS
value: "false"
- name: JWT_POLICY
value: {{ include "controller.jwtPolicy" . }}
- name: ISTIO_META_HTTP10
value: "1"
- name: ISTIO_META_CLUSTER_ID
value: "{{ $.Values.clusterName | default `Kubernetes` }}"
- name: INSTANCE_NAME
value: "higress-gateway"
{{- if .Values.global.liteMetrics }}
- name: LITE_METRICS
value: "on"
{{- end }}
{{- if include "skywalking.enabled" . }}
- name: ISTIO_BOOTSTRAP_OVERRIDE
value: /etc/istio/custom-bootstrap/custom_bootstrap.json
{{- end }}
{{- with .Values.gateway.networkGateway }}
- name: ISTIO_META_REQUESTED_NETWORK_VIEW
value: "{{.}}"
{{- end }}
{{- range $key, $val := .Values.env }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
ports:
- containerPort: 15090
protocol: TCP
name: http-envoy-prom
{{- if or .Values.global.local .Values.global.kind }}
- containerPort: {{ .Values.gateway.httpPort }}
hostPort: {{ .Values.gateway.httpPort }}
name: http
protocol: TCP
- containerPort: {{ .Values.gateway.httpsPort }}
hostPort: {{ .Values.gateway.httpsPort }}
name: https
protocol: TCP
{{- end }}
readinessProbe:
failureThreshold: {{ .Values.gateway.readinessFailureThreshold }}
httpGet:
path: /healthz/ready
port: 15021
scheme: HTTP
initialDelaySeconds: {{ .Values.gateway.readinessInitialDelaySeconds }}
periodSeconds: {{ .Values.gateway.readinessPeriodSeconds }}
successThreshold: {{ .Values.gateway.readinessSuccessThreshold }}
timeoutSeconds: {{ .Values.gateway.readinessTimeoutSeconds }}
{{- if not (or .Values.global.local .Values.global.kind) }}
resources:
{{- toYaml .Values.gateway.resources | nindent 12 }}
{{- end }}
volumeMounts:
{{- if eq (include "controller.jwtPolicy" .) "third-party-jwt" }}
- name: istio-token
mountPath: /var/run/secrets/tokens
readOnly: true
{{- end }}
- name: config
mountPath: /etc/istio/config
- name: istio-ca-root-cert
mountPath: /var/run/secrets/istio
- name: istio-data
mountPath: /var/lib/istio/data
- name: podinfo
mountPath: /etc/istio/pod
- name: proxy-socket
mountPath: /etc/istio/proxy
{{- if include "skywalking.enabled" . }}
- mountPath: /etc/istio/custom-bootstrap
name: custom-bootstrap-volume
{{- end }}
{{- if .Values.global.volumeWasmPlugins }}
- mountPath: /opt/plugins
name: local-wasmplugins-volume
{{- end }}
{{- if $o11y.enabled }}
- mountPath: /var/log/proxy
name: log
{{- end }}
{{- if .Values.gateway.hostNetwork }}
hostNetwork: {{ .Values.gateway.hostNetwork }}
dnsPolicy: ClusterFirstWithHostNet
{{- end }}
{{- with .Values.gateway.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.gateway.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.gateway.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- if eq (include "controller.jwtPolicy" .) "third-party-jwt" }}
- name: istio-token
projected:
sources:
- serviceAccountToken:
audience: istio-ca
expirationSeconds: 43200
path: istio-token
{{- end }}
- name: istio-ca-root-cert
configMap:
{{- if .Values.global.enableHigressIstio }}
name: istio-ca-root-cert
{{- else }}
name: higress-ca-root-cert
{{- end }}
- name: config
configMap:
name: higress-config
{{- if include "skywalking.enabled" . }}
- configMap:
defaultMode: 420
name: higress-custom-bootstrap
name: custom-bootstrap-volume
{{- end }}
- name: istio-data
emptyDir: {}
- name: proxy-socket
emptyDir: {}
{{- if $o11y.enabled }}
- name: log
emptyDir: {}
- name: tmp
emptyDir: {}
- name: promtail-config
configMap:
name: higress-promtail
{{- end }}
- name: podinfo
downwardAPI:
defaultMode: 420
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.labels
path: labels
- fieldRef:
apiVersion: v1
fieldPath: metadata.annotations
path: annotations
- path: cpu-request
resourceFieldRef:
containerName: higress-gateway
divisor: 1m
resource: requests.cpu
- path: cpu-limit
resourceFieldRef:
containerName: higress-gateway
divisor: 1m
resource: limits.cpu
{{- if .Values.global.volumeWasmPlugins }}
- name: local-wasmplugins-volume
hostPath:
path: /opt/plugins
type: Directory
{{- end }}
{{- end }}

View File

@@ -1,3 +1,4 @@
{{- if eq .Values.gateway.kind "Deployment" -}}
{{- $o11y := .Values.global.o11y }}
{{- $unprivilegedPortSupported := true }}
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
@@ -58,6 +59,9 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "gateway.serviceAccountName" . }}
{{- if .Values.global.priorityClassName }}
priorityClassName: "{{ .Values.global.priorityClassName }}"
{{- end }}
securityContext:
{{- if .Values.gateway.securityContext }}
{{- toYaml .Values.gateway.securityContext | nindent 8 }}
@@ -68,40 +72,6 @@ spec:
value: "0"
{{- end }}
containers:
{{- if $o11y.enabled }}
{{- $config := $o11y.promtail }}
- name: promtail
image: {{ $config.image.repository }}:{{ $config.image.tag }}
imagePullPolicy: IfNotPresent
args:
- -config.file=/etc/promtail/promtail.yaml
env:
- name: 'HOSTNAME'
valueFrom:
fieldRef:
fieldPath: 'spec.nodeName'
ports:
- containerPort: {{ $config.port }}
name: http-metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: {{ $config.port }}
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
volumeMounts:
- name: promtail-config
mountPath: "/etc/promtail"
- name: log
mountPath: /var/log/proxy
- name: tmp
mountPath: /tmp
{{- end }}
- name: higress-gateway
image: "{{ .Values.gateway.hub | default .Values.global.hub }}/{{ .Values.gateway.image | default "gateway" }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
args:
@@ -202,6 +172,9 @@ spec:
value: {{ $val | quote }}
{{- end }}
ports:
- containerPort: 15020
protocol: TCP
name: istio-prom
- containerPort: 15090
protocol: TCP
name: http-envoy-prom
@@ -241,7 +214,7 @@ spec:
mountPath: /var/run/secrets/istio
- name: istio-data
mountPath: /var/lib/istio/data
- name: podinfo
- name: podinfo
mountPath: /etc/istio/pod
- name: proxy-socket
mountPath: /etc/istio/proxy
@@ -257,6 +230,40 @@ spec:
- mountPath: /var/log/proxy
name: log
{{- end }}
{{- if $o11y.enabled }}
{{- $config := $o11y.promtail }}
- name: promtail
image: {{ $config.image.repository }}:{{ $config.image.tag }}
imagePullPolicy: IfNotPresent
args:
- -config.file=/etc/promtail/promtail.yaml
env:
- name: 'HOSTNAME'
valueFrom:
fieldRef:
fieldPath: 'spec.nodeName'
ports:
- containerPort: {{ $config.port }}
name: http-metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: {{ $config.port }}
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
volumeMounts:
- name: promtail-config
mountPath: "/etc/promtail"
- name: log
mountPath: /var/log/proxy
- name: tmp
mountPath: /tmp
{{- end }}
{{- if .Values.gateway.hostNetwork }}
hostNetwork: {{ .Values.gateway.hostNetwork }}
dnsPolicy: ClusterFirstWithHostNet
@@ -340,3 +347,4 @@ spec:
path: /opt/plugins
type: Directory
{{- end }}
{{- end }}

View File

@@ -0,0 +1,6 @@
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: {{ .Values.global.ingressClass }}
spec:
controller: higress.io/higress-controller

View File

@@ -15,6 +15,9 @@ spec:
{{- with .Values.gateway.service.loadBalancerIP }}
loadBalancerIP: "{{ . }}"
{{- end }}
{{- with .Values.gateway.service.loadBalancerClass }}
loadBalancerClass: "{{ . }}"
{{- end }}
{{- with .Values.gateway.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{ toYaml . | indent 4 }}

View File

@@ -1,6 +1,6 @@
revision: ""
global:
liteMetrics: false
liteMetrics: true
xdsMaxRecvMsgSize: "104857600"
defaultUpstreamConcurrencyThreshold: 10000
enableSRDS: true
@@ -178,9 +178,9 @@ global:
# Default port for Pilot agent health checks. A value of 0 will disable health checking.
statusPort: 15020
# Specify which tracer to use. One of: zipkin, lightstep, datadog, stackdriver.
# Specify which tracer to use. One of: lightstep, datadog, stackdriver.
# If using stackdriver tracer outside GCP, set env GOOGLE_APPLICATION_CREDENTIALS to the GCP credential file.
tracer: "zipkin"
tracer: ""
# Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
holdApplicationUntilProxyStarts: false
@@ -330,12 +330,8 @@ global:
maxNumberOfAnnotations: 200
# The global default max number of attributes per span.
maxNumberOfAttributes: 200
zipkin:
# Host:Port for reporting trace data in zipkin format. If not specified, will default to
# zipkin service (port 9411) in the same namespace as the other istio components.
address: ""
# Use the Mesh Control Protocol (MCP) for configuring Istiod. Requires an MCP source.
useMCP: false
# Observability (o11y) configurations
@@ -343,7 +339,7 @@ global:
enabled: false
promtail:
image:
repository: grafana/promtail
repository: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/promtail
tag: 2.9.4
port: 3101
resources:
@@ -396,6 +392,9 @@ gateway:
replicas: 2
image: gateway
# -- Use a `DaemonSet` or `Deployment`
kind: Deployment
# The number of successive failed probes before indicating readiness failure.
readinessFailureThreshold: 30
@@ -468,6 +467,7 @@ gateway:
targetPort: 443
annotations: {}
loadBalancerIP: ""
loadBalancerClass: ""
loadBalancerSourceRanges: []
externalTrafficPolicy: ""
@@ -589,7 +589,7 @@ controller:
maxReplicas: 5
targetCPUUtilizationPercentage: 80
automaticHttps:
enabled: false
enabled: true
email: ""
## Discovery Settings
@@ -664,9 +664,15 @@ pilot:
podLabels: {}
# Skywalking config settings
skywalking:
enabled: false
service:
address: ~
port: 11800
# Tracing config settings
tracing:
enable: false
sampling: 100
timeout: 500
skywalking:
# access_token: ""
service: ""
port: 11800
# zipkin:
# service: ""
# port: 9411

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 1.4.0
version: 1.4.2
- name: higress-console
repository: https://higress.io/helm-charts/
version: 1.4.0
digest: sha256:bf4c58ac28d4691907eab44a13eee398fc05ade95cdae07cb91d7e20ce4ba382
generated: "2024-05-29T21:18:32.791995+08:00"
version: 1.4.2
digest: sha256:31b557e55584e589b140ae9b89cfc8b99df91771c7d28465c3a2b06a4f35a192
generated: "2024-07-26T13:53:23.225023+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.4.0
appVersion: 1.4.2
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 1.4.0
version: 1.4.2
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 1.4.0
version: 1.4.2
type: application
version: 1.4.0
version: 1.4.2

View File

@@ -0,0 +1,21 @@
diff -Naur istio/tools/packaging/common/envoy_bootstrap.json istio-new/tools/packaging/common/envoy_bootstrap.json
--- istio/tools/packaging/common/envoy_bootstrap.json 2024-06-07 16:50:21.000000000 +0800
+++ istio-new/tools/packaging/common/envoy_bootstrap.json 2024-06-07 16:47:42.000000000 +0800
@@ -38,7 +38,7 @@
"stats_tags": [
{
"tag_name": "cluster_name",
- "regex": "^cluster\\.((.+?(\\..+?\\.svc\\.cluster\\.local)?)\\.)"
+ "regex": "^cluster\\.((.*?)\\.)(http1\\.|http2\\.|health_check\\.|zone\\.|external\\.|circuit_breakers\\.|[^\\.]+$)"
},
{
"tag_name": "tcp_prefix",
@@ -58,7 +58,7 @@
},
{
"tag_name": "http_conn_manager_prefix",
- "regex": "^http\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
+ "regex": "^http\\.(((outbound_([0-9]{1,3}\\.{0,1}){4}_\\d{0,5})|([^\\.]+))\\.)"
},
{
"tag_name": "listener_address",

View File

@@ -0,0 +1,53 @@
diff -Naur istio/tools/packaging/common/envoy_bootstrap.json istio-new/tools/packaging/common/envoy_bootstrap.json
--- istio/tools/packaging/common/envoy_bootstrap.json 2024-06-19 13:39:49.179159469 +0800
+++ istio-new/tools/packaging/common/envoy_bootstrap.json 2024-06-19 13:39:28.299159059 +0800
@@ -37,6 +37,18 @@
"use_all_default_tags": false,
"stats_tags": [
{
+ "tag_name": "ai_route",
+ "regex": "^wasmcustom\\.route\\.((.*?)\\.)upstream"
+ },
+ {
+ "tag_name": "ai_cluster",
+ "regex": "^wasmcustom\\..*?\\.upstream\\.((.*?)\\.)model"
+ },
+ {
+ "tag_name": "ai_model",
+ "regex": "^wasmcustom\\..*?\\.model\\.((.*?)\\.)(input_token|output_token)"
+ },
+ {
"tag_name": "cluster_name",
"regex": "^cluster\\.((.*?)\\.)(http1\\.|http2\\.|health_check\\.|zone\\.|external\\.|circuit_breakers\\.|[^\\.]+$)"
},
diff -Naur istio/tools/packaging/common/envoy_bootstrap_lite.json istio-new/tools/packaging/common/envoy_bootstrap_lite.json
--- istio/tools/packaging/common/envoy_bootstrap_lite.json 2024-06-19 13:39:49.175159469 +0800
+++ istio-new/tools/packaging/common/envoy_bootstrap_lite.json 2024-06-19 13:38:52.283158352 +0800
@@ -37,6 +37,18 @@
"use_all_default_tags": false,
"stats_tags": [
{
+ "tag_name": "ai_route",
+ "regex": "^wasmcustom\\.route\\.((.*?)\\.)upstream"
+ },
+ {
+ "tag_name": "ai_cluster",
+ "regex": "^wasmcustom\\..*?\\.upstream\\.((.*?)\\.)model"
+ },
+ {
+ "tag_name": "ai_model",
+ "regex": "^wasmcustom\\..*?\\.model\\.((.*?)\\.)(input_token|output_token)"
+ },
+ {
"tag_name": "response_code_class",
"regex": "_rq(_(\\dxx))$"
},
@@ -60,7 +72,7 @@
"prefix": "vhost"
},
{
- "safe_regex": {"regex": "^http.*rds.*", "google_re2":{}}
+ "safe_regex": {"regex": "^http.*\\.rds\\..*", "google_re2":{}}
}
]
}

View File

@@ -391,7 +391,7 @@ func (s *Server) initAutomaticHttps() error {
ServerAddress: s.CertHttpAddress,
Email: s.AutomaticHttpsEmail,
}
certServer, err := cert.NewServer(s.kubeClient.Kube(), certOption)
certServer, err := cert.NewServer(s.kubeClient.Kube(), s.xdsServer, certOption)
if err != nil {
return err
}

View File

@@ -17,10 +17,15 @@ package cert
import (
"context"
"fmt"
"os"
"reflect"
"sync"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"istio.io/istio/pilot/pkg/model"
"k8s.io/client-go/kubernetes"
)
@@ -28,6 +33,10 @@ const (
EventCertObtained = "cert_obtained"
)
var (
cfg *certmagic.Config
)
type CertMgr struct {
cfg *certmagic.Config
client kubernetes.Interface
@@ -39,9 +48,10 @@ type CertMgr struct {
ingressSolver acmez.Solver
configMgr *ConfigMgr
secretMgr *SecretMgr
XDSUpdater model.XDSUpdater
}
func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (*CertMgr, error) {
func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config, XDSUpdater model.XDSUpdater, configMgr *ConfigMgr) (*CertMgr, error) {
CertLog.Infof("certmgr init config: %+v", config)
// Init certmagic config
// First make a pointer to a Cache as we need to reference the same Cache in
@@ -49,21 +59,29 @@ func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (
var cache *certmagic.Cache
var storage certmagic.Storage
storage, _ = NewConfigmapStorage(opts.Namespace, clientSet)
renewalWindowRatio := float64(config.RenewBeforeDays / RenewMaxDays)
renewalWindowRatio := float64(config.RenewBeforeDays) / float64(RenewMaxDays)
logger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
os.Stderr,
zap.DebugLevel,
))
magicConfig := certmagic.Config{
RenewalWindowRatio: renewalWindowRatio,
Storage: storage,
Logger: logger,
}
cache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
// Here we use New to get a valid Config associated with the same cache.
// The provided Config is used as a template and will be completed with
// any defaults that are set in the Default config.
return certmagic.New(cache, magicConfig), nil
return cfg, nil
},
Logger: logger,
})
// init certmagic
cfg := certmagic.New(cache, magicConfig)
cfg = certmagic.New(cache, magicConfig)
// Init certmagic acme
issuer := config.GetIssuer(IssuerTypeLetsencrypt)
if issuer == nil {
@@ -85,7 +103,6 @@ func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (
// init issuers
cfg.Issuers = []certmagic.Issuer{myACME}
configMgr, _ := NewConfigMgr(opts.Namespace, clientSet)
secretMgr, _ := NewSecretMgr(opts.Namespace, clientSet)
certMgr := &CertMgr{
@@ -97,6 +114,7 @@ func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (
configMgr: configMgr,
secretMgr: secretMgr,
cache: cache,
XDSUpdater: XDSUpdater,
}
certMgr.cfg.OnEvent = certMgr.OnEvent
return certMgr, nil
@@ -149,18 +167,31 @@ func (s *CertMgr) Reconcile(ctx context.Context, oldConfig *Config, newConfig *C
// sync email
s.myACME.Email = newIssuer.Email
// sync RenewalWindowRatio
s.cfg.RenewalWindowRatio = float64(newConfig.RenewBeforeDays / RenewMaxDays)
renewalWindowRatio := float64(newConfig.RenewBeforeDays) / float64(RenewMaxDays)
s.cfg.RenewalWindowRatio = renewalWindowRatio
// start cache
s.cache.Start()
// sync domains
s.manageSync(context.Background(), newDomains)
s.configMgr.SetConfig(newConfig)
CertLog.Infof("certMgr start to manageSync domains:+v%", newDomains)
s.manageSync(context.Background(), newDomains)
CertLog.Infof("certMgr manageSync domains done")
} else {
// stop cache maintainAssets
s.cache.Stop()
s.configMgr.SetConfig(newConfig)
}
if oldConfig != nil && newConfig != nil {
if oldConfig.FallbackForInvalidSecret != newConfig.FallbackForInvalidSecret || !reflect.DeepEqual(oldConfig.CredentialConfig, newConfig.CredentialConfig) {
CertLog.Infof("ingress need to full push")
s.XDSUpdater.ConfigUpdate(&model.PushRequest{
Full: true,
Reason: []model.TriggerReason{"higress-https-updated"},
})
}
}
return nil
}

View File

@@ -45,11 +45,12 @@ const (
// Config is the configuration of automatic https.
type Config struct {
AutomaticHttps bool `json:"automaticHttps"`
RenewBeforeDays int `json:"renewBeforeDays"`
CredentialConfig []CredentialEntry `json:"credentialConfig"`
ACMEIssuer []ACMEIssuerEntry `json:"acmeIssuer"`
Version string `json:"version"`
AutomaticHttps bool `json:"automaticHttps"`
FallbackForInvalidSecret bool `json:"fallbackForInvalidSecret"`
RenewBeforeDays int `json:"renewBeforeDays"`
CredentialConfig []CredentialEntry `json:"credentialConfig"`
ACMEIssuer []ACMEIssuerEntry `json:"acmeIssuer"`
Version string `json:"version"`
}
func (c *Config) GetIssuer(issuerName IssuerName) *ACMEIssuerEntry {
@@ -85,22 +86,35 @@ func (c *Config) GetSecretNameByDomain(issuerName IssuerName, domain string) str
return ""
}
func ParseTLSSecret(tlsSecret string) (string, string) {
secrets := strings.Split(tlsSecret, "/")
switch len(secrets) {
case 1:
return "", tlsSecret
case 2:
return secrets[0], secrets[1]
}
return "", ""
}
func (c *Config) Validate() error {
// check acmeIssuer
if len(c.ACMEIssuer) == 0 {
return fmt.Errorf("acmeIssuer is empty")
}
for _, issuer := range c.ACMEIssuer {
switch issuer.Name {
case IssuerTypeLetsencrypt:
if issuer.Email == "" {
return fmt.Errorf("acmeIssuer %s email is empty", issuer.Name)
if c.AutomaticHttps {
if len(c.ACMEIssuer) == 0 {
return fmt.Errorf("no acmeIssuer configuration found when automaticHttps is enable")
}
for _, issuer := range c.ACMEIssuer {
switch issuer.Name {
case IssuerTypeLetsencrypt:
if issuer.Email == "" {
return fmt.Errorf("acmeIssuer %s email is empty", issuer.Name)
}
if !ValidateEmail(issuer.Email) {
return fmt.Errorf("acmeIssuer %s email %s is invalid", issuer.Name, issuer.Email)
}
default:
return fmt.Errorf("acmeIssuer name %s is not supported", issuer.Name)
}
if !ValidateEmail(issuer.Email) {
return fmt.Errorf("acmeIssuer %s email %s is invalid", issuer.Name, issuer.Email)
}
default:
return fmt.Errorf("acmeIssuer name %s is not supported", issuer.Name)
}
}
// check credentialConfig
@@ -110,14 +124,20 @@ func (c *Config) Validate() error {
}
if credential.TLSSecret == "" {
return fmt.Errorf("credentialConfig tlsSecret is empty")
} else {
ns, secret := ParseTLSSecret(credential.TLSSecret)
if ns == "" && secret == "" {
return fmt.Errorf("credentialConfig tlsSecret %s is not supported", credential.TLSSecret)
}
}
if credential.TLSIssuer == IssuerTypeLetsencrypt {
if len(credential.Domains) > 1 {
return fmt.Errorf("credentialConfig tlsIssuer %s only support one domain", credential.TLSIssuer)
}
}
if credential.TLSIssuer != IssuerTypeLetsencrypt && len(credential.TLSIssuer) > 0 {
return fmt.Errorf("credential tls issuer %s is not support", credential.TLSIssuer)
return fmt.Errorf("credential tls issuer %s is not supported", credential.TLSIssuer)
}
}
@@ -274,11 +294,12 @@ func newDefaultConfig(email string) *Config {
}
defaultCredentialConfig := make([]CredentialEntry, 0)
config := &Config{
AutomaticHttps: true,
RenewBeforeDays: DefaultRenewBeforeDays,
ACMEIssuer: defaultIssuer,
CredentialConfig: defaultCredentialConfig,
Version: time.Now().Format("20060102030405"),
AutomaticHttps: true,
FallbackForInvalidSecret: false,
RenewBeforeDays: DefaultRenewBeforeDays,
ACMEIssuer: defaultIssuer,
CredentialConfig: defaultCredentialConfig,
Version: time.Now().Format("20060102030405"),
}
return config
}

View File

@@ -120,3 +120,36 @@ func TestMatchSecretNameByDomain(t *testing.T) {
})
}
}
func TestParseTLSSecret(t *testing.T) {
tests := []struct {
tlsSecret string
expectedNamespace string
expectedSecretName string
}{
{
tlsSecret: "example-com-tls",
expectedNamespace: "",
expectedSecretName: "example-com-tls",
},
{
tlsSecret: "kube-system/example-com-tls",
expectedNamespace: "kube-system",
expectedSecretName: "example-com-tls",
},
{
tlsSecret: "kube-system/example-com/wildcard",
expectedNamespace: "",
expectedSecretName: "",
},
}
for _, tt := range tests {
t.Run(tt.tlsSecret, func(t *testing.T) {
resultNamespace, resultSecretName := ParseTLSSecret(tt.tlsSecret)
assert.Equal(t, tt.expectedNamespace, resultNamespace)
assert.Equal(t, tt.expectedSecretName, resultSecretName)
})
}
}

View File

@@ -18,7 +18,6 @@ import (
"context"
"fmt"
"strconv"
"strings"
"time"
v1 "k8s.io/api/core/v1"
@@ -27,10 +26,6 @@ import (
"k8s.io/client-go/kubernetes"
)
const (
SecretNamePrefix = "higress-secret-"
)
type SecretMgr struct {
client kubernetes.Interface
namespace string
@@ -46,13 +41,21 @@ func NewSecretMgr(namespace string, client kubernetes.Interface) (*SecretMgr, er
}
func (s *SecretMgr) Update(domain string, secretName string, privateKey []byte, certificate []byte, notBefore time.Time, notAfter time.Time, isRenew bool) error {
//secretName := s.getSecretName(domain)
secret := s.constructSecret(domain, privateKey, certificate, notBefore, notAfter, isRenew)
_, err := s.client.CoreV1().Secrets(s.namespace).Get(context.Background(), secretName, metav1.GetOptions{})
CertLog.Infof("update secret, domain:%s, secretName:%s, notBefore:%v, notAfter:%v, isRenew:%t", domain, secretName, notBefore, notAfter, isRenew)
name := secretName
namespace := s.namespace
namespaceP, secretP := ParseTLSSecret(secretName)
if namespaceP != "" {
namespace = namespaceP
name = secretP
}
secret := s.constructSecret(domain, name, namespace, privateKey, certificate, notBefore, notAfter, isRenew)
_, err := s.client.CoreV1().Secrets(namespace).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
// create secret
_, err2 := s.client.CoreV1().Secrets(s.namespace).Create(context.Background(), secret, metav1.CreateOptions{})
_, err2 := s.client.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{})
return err2
}
return err
@@ -61,7 +64,7 @@ func (s *SecretMgr) Update(domain string, secretName string, privateKey []byte,
if _, ok := secret.Annotations["higress.io/cert-domain"]; !ok {
return fmt.Errorf("the secret name %s is not automatic https secret name for the domain:%s, please rename it in config", secretName, domain)
}
_, err1 := s.client.CoreV1().Secrets(s.namespace).Update(context.Background(), secret, metav1.UpdateOptions{})
_, err1 := s.client.CoreV1().Secrets(namespace).Update(context.Background(), secret, metav1.UpdateOptions{})
if err1 != nil {
return err1
}
@@ -69,23 +72,13 @@ func (s *SecretMgr) Update(domain string, secretName string, privateKey []byte,
return nil
}
func (s *SecretMgr) Delete(domain string) error {
secretName := s.getSecretName(domain)
err := s.client.CoreV1().Secrets(s.namespace).Delete(context.Background(), secretName, metav1.DeleteOptions{})
return err
}
func (s *SecretMgr) getSecretName(domain string) string {
return SecretNamePrefix + strings.ReplaceAll(strings.TrimSpace(domain), ".", "-")
}
func (s *SecretMgr) constructSecret(domain string, privateKey []byte, certificate []byte, notBefore time.Time, notAfter time.Time, isRenew bool) *v1.Secret {
secretName := s.getSecretName(domain)
func (s *SecretMgr) constructSecret(domain string, name string, namespace string, privateKey []byte, certificate []byte, notBefore time.Time, notAfter time.Time, isRenew bool) *v1.Secret {
annotationMap := make(map[string]string, 0)
annotationMap["higress.io/cert-domain"] = domain
annotationMap["higress.io/cert-notAfter"] = notAfter.Format("2006-01-02 15:04:05")
annotationMap["higress.io/cert-notBefore"] = notBefore.Format("2006-01-02 15:04:05")
annotationMap["higress.io/cert-renew"] = strconv.FormatBool(isRenew)
annotationMap["higress.io/cert-source"] = string(IssuerTypeLetsencrypt)
if isRenew {
annotationMap["higress.io/cert-renew-time"] = time.Now().Format("2006-01-02 15:04:05")
}
@@ -97,8 +90,8 @@ func (s *SecretMgr) constructSecret(domain string, privateKey []byte, certificat
dataMap["tls.crt"] = certificate
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: s.namespace,
Name: name,
Namespace: namespace,
Annotations: annotationMap,
},
Type: v1.SecretTypeTLS,

View File

@@ -22,6 +22,7 @@ import (
"time"
"github.com/caddyserver/certmagic"
"istio.io/istio/pilot/pkg/model"
"k8s.io/client-go/kubernetes"
)
@@ -37,12 +38,14 @@ type Server struct {
clientSet kubernetes.Interface
controller *Controller
certMgr *CertMgr
XDSUpdater model.XDSUpdater
}
func NewServer(clientSet kubernetes.Interface, opts *Option) (*Server, error) {
func NewServer(clientSet kubernetes.Interface, XDSUpdater model.XDSUpdater, opts *Option) (*Server, error) {
server := &Server{
clientSet: clientSet,
opts: opts,
clientSet: clientSet,
opts: opts,
XDSUpdater: XDSUpdater,
}
return server, nil
}
@@ -65,7 +68,7 @@ func (s *Server) InitServer() error {
return err
}
// init certmgr
certMgr, err := InitCertMgr(s.opts, s.clientSet, defaultConfig) // config and start
certMgr, err := InitCertMgr(s.opts, s.clientSet, defaultConfig, s.XDSUpdater, configMgr) // config and start
s.certMgr = certMgr
// init controller
controller, err := NewController(s.clientSet, s.opts.Namespace, certMgr, configMgr)

View File

@@ -32,7 +32,7 @@ import (
)
const (
CertificatesPrefix = "/certificates"
CertificatesPrefix = "certificates"
ConfigmapStoreCertficatesPrefix = "higress-cert-store-certificates-"
ConfigmapStoreDefaultName = "higress-cert-store-default"
)
@@ -155,7 +155,7 @@ func (s *ConfigmapStorage) List(ctx context.Context, prefix string, recursive bo
// Check if the prefix corresponds to a specific key
hashPrefix := fastHash([]byte(prefix))
if strings.HasPrefix(prefix, CertificatesPrefix) {
// If the prefix is "/certificates", get all ConfigMaps and traverse each one
// If the prefix is "certificates/", get all ConfigMaps and traverse each one
// List all ConfigMaps in the namespace with label higress.io/cert-https=true
configmaps, err := s.client.CoreV1().ConfigMaps(s.namespace).List(ctx, metav1.ListOptions{FieldSelector: "metadata.annotations['higress.io/cert-https'] == 'true'"})
if err != nil {
@@ -289,14 +289,29 @@ func (s *ConfigmapStorage) String() string {
return "ConfigmapStorage"
}
// getConfigmapStoreNameByKey determines the storage name for a given key.
// It checks if the key starts with 'certificates/' and if so, the key pattern should match one of the following:
// 'certificates/<issuerKey>/<domain>/<domain>.json',
// 'certificates/<issuerKey>/<domain>/<domain>.crt',
// or 'certificates/<issuerKey>/<domain>/<domain>.key'.
// It then returns the corresponding ConfigMap name.
// If the key does not start with 'certificates/', it returns the default store name.
//
// Parameters:
//
// key - The configuration map key that needs to be mapped to a storage name.
//
// Returns:
//
// string - The calculated or default storage name based on the key.
func (s *ConfigmapStorage) getConfigmapStoreNameByKey(key string) string {
parts := strings.SplitN(key, "/", 10)
if len(parts) >= 4 && parts[1] == "certificates" {
domain := strings.TrimSuffix(parts[3], ".crt")
domain = strings.TrimSuffix(domain, ".key")
domain = strings.TrimSuffix(domain, ".json")
issuerKey := parts[2]
return ConfigmapStoreCertficatesPrefix + fastHash([]byte(issuerKey+domain))
if strings.HasPrefix(key, "certificates/") {
parts := strings.Split(key, "/")
if len(parts) >= 4 && parts[0] == "certificates" {
domain := parts[2]
issuerKey := parts[1]
return ConfigmapStoreCertficatesPrefix + fastHash([]byte(issuerKey+domain))
}
}
return ConfigmapStoreDefaultName
}

View File

@@ -39,22 +39,29 @@ func TestGetConfigmapStoreNameByKey(t *testing.T) {
}{
{
name: "certificate crt",
key: "/certificates/issuerKey/domain.crt",
key: "certificates/issuerKey/domain/domain.crt",
expected: "higress-cert-store-certificates-" + fastHash([]byte("issuerKey"+"domain")),
},
{
name: "47.237.14.136.sslip.io crt",
key: "certificates/acme-v02.api.letsencrypt.org-directory/47.237.14.136.sslip.io/47.237.14.136.sslip.io.crt",
expected: "higress-cert-store-certificates-" + fastHash([]byte("acme-v02.api.letsencrypt.org-directory"+"47.237.14.136.sslip.io")),
},
{
name: "certificate meta",
key: "/certificates/issuerKey/domain.json",
key: "certificates/issuerKey/domain/domain.json",
expected: "higress-cert-store-certificates-" + fastHash([]byte("issuerKey"+"domain")),
},
{
name: "certificate key",
key: "/certificates/issuerKey/domain.key",
key: "certificates/issuerKey/domain/domain.key",
expected: "higress-cert-store-certificates-" + fastHash([]byte("issuerKey"+"domain")),
},
{
name: "user key",
key: "/users/hello/2",
key: "users/hello/2",
expected: "higress-cert-store-default",
},
{
@@ -82,7 +89,7 @@ func TestExists(t *testing.T) {
assert.NoError(t, err)
// Store a test key
testKey := "/certificates/issuer1/domain1.crt"
testKey := "certificates/issuer1/domain1/domain1.crt"
err = storage.Store(context.Background(), testKey, []byte("test-data"))
assert.NoError(t, err)
@@ -94,17 +101,17 @@ func TestExists(t *testing.T) {
}{
{
name: "Existing Key",
key: "/certificates/issuer1/domain1.crt",
key: "certificates/issuer1/domain1/domain1.crt",
shouldExist: true,
},
{
name: "Non-Existent Key1",
key: "/certificates/issuer2/domain2.crt",
key: "certificates/issuer2/domain2/domain2.crt",
shouldExist: false,
},
{
name: "Non-Existent Key2",
key: "/users/hello/a",
key: "users/hello/a",
shouldExist: false,
},
// Add more test cases as needed
@@ -129,7 +136,7 @@ func TestLoad(t *testing.T) {
assert.NoError(t, err)
// Store a test key
testKey := "/certificates/issuer1/domain1.crt"
testKey := "certificates/issuer1/domain1/domain1.crt"
testValue := []byte("test-data")
err = storage.Store(context.Background(), testKey, testValue)
assert.NoError(t, err)
@@ -143,13 +150,13 @@ func TestLoad(t *testing.T) {
}{
{
name: "Existing Key",
key: "/certificates/issuer1/domain1.crt",
key: "certificates/issuer1/domain1/domain1.crt",
expected: testValue,
shouldError: false,
},
{
name: "Non-Existent Key",
key: "/certificates/issuer2/domain2.crt",
key: "certificates/issuer2/domain2/domain2.crt",
expected: nil,
shouldError: true,
},
@@ -192,28 +199,28 @@ func TestStore(t *testing.T) {
shouldError bool
}{
{
name: "Store Key with /certificates prefix",
key: "/certificates/issuer1/domain1.crt",
name: "Store Key with certificates prefix",
key: "certificates/issuer1/domain1/domain1.crt",
value: []byte("test-data1"),
expected: map[string]string{fastHash([]byte("/certificates/issuer1/domain1.crt")): `{"k":"/certificates/issuer1/domain1.crt","v":"dGVzdC1kYXRhMQ=="}`},
expected: map[string]string{fastHash([]byte("certificates/issuer1/domain1/domain1.crt")): `{"k":"certificates/issuer1/domain1/domain1.crt","v":"dGVzdC1kYXRhMQ=="}`},
expectedConfigmapName: "higress-cert-store-certificates-" + fastHash([]byte("issuer1"+"domain1")),
shouldError: false,
},
{
name: "Store Key with /certificates prefix (additional data)",
key: "/certificates/issuer2/domain2.crt",
name: "Store Key with certificates prefix (additional data)",
key: "certificates/issuer2/domain2/domain2.crt",
value: []byte("test-data2"),
expected: map[string]string{
fastHash([]byte("/certificates/issuer2/domain2.crt")): `{"k":"/certificates/issuer2/domain2.crt","v":"dGVzdC1kYXRhMg=="}`,
fastHash([]byte("certificates/issuer2/domain2/domain2.crt")): `{"k":"certificates/issuer2/domain2/domain2.crt","v":"dGVzdC1kYXRhMg=="}`,
},
expectedConfigmapName: "higress-cert-store-certificates-" + fastHash([]byte("issuer2"+"domain2")),
shouldError: false,
},
{
name: "Store Key without /certificates prefix",
key: "/other/path/data.txt",
name: "Store Key without certificates prefix",
key: "other/path/data.txt",
value: []byte("test-data3"),
expected: map[string]string{fastHash([]byte("/other/path/data.txt")): `{"k":"/other/path/data.txt","v":"dGVzdC1kYXRhMw=="}`},
expected: map[string]string{fastHash([]byte("other/path/data.txt")): `{"k":"other/path/data.txt","v":"dGVzdC1kYXRhMw=="}`},
expectedConfigmapName: "higress-cert-store-default",
shouldError: false,
},
@@ -256,17 +263,17 @@ func TestList(t *testing.T) {
// Store some test data
// Store some test data
testKeys := []string{
"/certificates/issuer1/domain1.crt",
"/certificates/issuer1/domain2.crt",
"/certificates/issuer1/domain3.crt", // Added another domain for issuer1
"/certificates/issuer2/domain4.crt",
"/certificates/issuer2/domain5.crt",
"/certificates/issuer3/subdomain1/domain6.crt", // Two-level subdirectory under issuer3
"/certificates/issuer3/subdomain1/subdomain2/domain7.crt", // Two more levels under issuer3
"/other-prefix/key1/file1",
"/other-prefix/key1/file2",
"/other-prefix/key2/file3",
"/other-prefix/key2/file4",
"certificates/issuer1/domain1/domain1.crt",
"certificates/issuer1/domain2/domain2.crt",
"certificates/issuer1/domain3/domain3.crt", // Added another domain for issuer1
"certificates/issuer2/domain4/domain4.crt",
"certificates/issuer2/domain5/domain5.crt",
"certificates/issuer3/domain6/domain6.crt", // Two-level subdirectory under issuer3
"certificates/issuer3/subdomain1/subdomain2/domain7.crt", // Two more levels under issuer3
"other-prefix/key1/file1",
"other-prefix/key1/file2",
"other-prefix/key2/file3",
"other-prefix/key2/file4",
}
for _, key := range testKeys {
@@ -283,34 +290,34 @@ func TestList(t *testing.T) {
}{
{
name: "List Certificates (Non-Recursive)",
prefix: "/certificates",
prefix: "certificates",
recursive: false,
expected: []string{"/certificates/issuer1", "/certificates/issuer2", "/certificates/issuer3"},
expected: []string{"certificates/issuer1", "certificates/issuer2", "certificates/issuer3"},
},
{
name: "List Certificates (Recursive)",
prefix: "/certificates",
prefix: "certificates",
recursive: true,
expected: []string{"/certificates/issuer1/domain1.crt", "/certificates/issuer1/domain2.crt", "/certificates/issuer1/domain3.crt", "/certificates/issuer2/domain4.crt", "/certificates/issuer2/domain5.crt", "/certificates/issuer3/subdomain1/domain6.crt", "/certificates/issuer3/subdomain1/subdomain2/domain7.crt"},
expected: []string{"certificates/issuer1/domain1/domain1.crt", "certificates/issuer1/domain2/domain2.crt", "certificates/issuer1/domain3/domain3.crt", "certificates/issuer2/domain4/domain4.crt", "certificates/issuer2/domain5/domain5.crt", "certificates/issuer3/domain6/domain6.crt", "certificates/issuer3/subdomain1/subdomain2/domain7.crt"},
},
{
name: "List Other Prefix (Non-Recursive)",
prefix: "/other-prefix",
prefix: "other-prefix",
recursive: false,
expected: []string{"/other-prefix/key1", "/other-prefix/key2"},
expected: []string{"other-prefix/key1", "other-prefix/key2"},
},
{
name: "List Other Prefix (Non-Recursive)",
prefix: "/other-prefix/key1",
prefix: "other-prefix/key1",
recursive: false,
expected: []string{"/other-prefix/key1/file1", "/other-prefix/key1/file2"},
expected: []string{"other-prefix/key1/file1", "other-prefix/key1/file2"},
},
{
name: "List Other Prefix (Recursive)",
prefix: "/other-prefix",
prefix: "other-prefix",
recursive: true,
expected: []string{"/other-prefix/key1/file1", "/other-prefix/key1/file2", "/other-prefix/key2/file3", "/other-prefix/key2/file4"},
expected: []string{"other-prefix/key1/file1", "other-prefix/key1/file2", "other-prefix/key2/file3", "other-prefix/key2/file4"},
},
}

View File

@@ -15,7 +15,8 @@
package hgctl
const (
yamlOutput = "yaml"
jsonOutput = "json"
flagsOutput = "flags"
summaryOutput = "short"
yamlOutput = "yaml"
jsonOutput = "json"
flagsOutput = "flags"
)

View File

@@ -19,6 +19,7 @@ import (
"github.com/alibaba/higress/cmd/hgctl/config"
"github.com/spf13/cobra"
"istio.io/istio/istioctl/pkg/writer/envoy/configdump"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
@@ -49,17 +50,23 @@ func runClusterConfig(c *cobra.Command, args []string) error {
if len(args) != 0 {
podName = args[0]
}
envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{
configWriter, err := config.GetEnvoyConfigWriter(&config.GetEnvoyConfigOptions{
PodName: podName,
PodNamespace: podNamespace,
BindAddress: bindAddress,
Output: output,
EnvoyConfigType: config.ClusterEnvoyConfigType,
IncludeEds: true,
})
}, c.OutOrStdout())
if err != nil {
return err
}
_, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig))
return err
switch output {
case summaryOutput:
return configWriter.PrintClusterSummary(configdump.ClusterFilter{})
case jsonOutput, yamlOutput:
return configWriter.PrintClusterDump(configdump.ClusterFilter{}, output)
default:
return fmt.Errorf("output format %q not supported", output)
}
}

View File

@@ -52,7 +52,7 @@ func newConfigCommand() *cobra.Command {
flags := cfgCommand.Flags()
options.AddKubeConfigFlags(flags)
cfgCommand.PersistentFlags().StringVarP(&output, "output", "o", "json", "One of 'yaml' or 'json'")
cfgCommand.PersistentFlags().StringVarP(&output, "output", "o", "json", "Output format: one of json|yaml|short")
cfgCommand.PersistentFlags().StringVarP(&podNamespace, "namespace", "n", "higress-system", "Namespace where envoy proxy pod are installed.")
return cfgCommand

View File

@@ -19,6 +19,7 @@ import (
"github.com/alibaba/higress/cmd/hgctl/config"
"github.com/spf13/cobra"
"istio.io/istio/istioctl/pkg/writer/envoy/configdump"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
@@ -49,17 +50,23 @@ func runListenerConfig(c *cobra.Command, args []string) error {
if len(args) != 0 {
podName = args[0]
}
envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{
configWriter, err := config.GetEnvoyConfigWriter(&config.GetEnvoyConfigOptions{
PodName: podName,
PodNamespace: podNamespace,
BindAddress: bindAddress,
Output: output,
EnvoyConfigType: config.ListenerEnvoyConfigType,
IncludeEds: true,
})
}, c.OutOrStdout())
if err != nil {
return err
}
_, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig))
return err
switch output {
case summaryOutput:
return configWriter.PrintListenerSummary(configdump.ListenerFilter{Verbose: true})
case jsonOutput, yamlOutput:
return configWriter.PrintListenerDump(configdump.ListenerFilter{Verbose: true}, output)
default:
return fmt.Errorf("output format %q not supported", output)
}
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/alibaba/higress/cmd/hgctl/config"
"github.com/spf13/cobra"
"istio.io/istio/istioctl/pkg/writer/envoy/configdump"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
@@ -49,17 +50,23 @@ func runRouteConfig(c *cobra.Command, args []string) error {
if len(args) != 0 {
podName = args[0]
}
envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{
configWriter, err := config.GetEnvoyConfigWriter(&config.GetEnvoyConfigOptions{
PodName: podName,
PodNamespace: podNamespace,
BindAddress: bindAddress,
Output: output,
EnvoyConfigType: config.RouteEnvoyConfigType,
IncludeEds: true,
})
}, c.OutOrStdout())
if err != nil {
return err
}
_, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig))
return err
switch output {
case summaryOutput:
return configWriter.PrintRouteSummary(configdump.RouteFilter{Verbose: true})
case jsonOutput, yamlOutput:
return configWriter.PrintRouteDump(configdump.RouteFilter{Verbose: true}, output)
default:
return fmt.Errorf("output format %q not supported", output)
}
}

View File

@@ -425,7 +425,7 @@ func openCommand(writer io.Writer, command string, args ...string) {
_, err := exec.LookPath(command)
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
fmt.Fprintf(writer, "Could not open your browser. Please open it maually.\n")
fmt.Fprintf(writer, "Could not open your browser. Please open it manually.\n")
return
}
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", args[0], err.Error())

View File

@@ -28,7 +28,7 @@ import (
const (
setFlagHelpStr = `Override an higress profile value, e.g. to choose a profile
(--set profile=local-k8s), or override profile values (--set gateway.replicas=2), or override helm values (--set values.global.proxy.resources.requsts.cpu=500m).`
(--set profile=local-k8s), or override profile values (--set gateway.replicas=2), or override helm values (--set values.global.proxy.resources.requests.cpu=500m).`
// manifestsFlagHelpStr is the command line description for --manifests
manifestsFlagHelpStr = `Specify a path to a directory of profiles
(e.g. ~/Downloads/higress/manifests).`
@@ -101,7 +101,7 @@ func newInstallCmd() *cobra.Command {
hgctl install --set profile=local-k8s --set global.enableIstioAPI=true --set gateway.replicas=2"
# To override helm setting
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requsts.cpu=500m"
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requests.cpu=500m"
`,
@@ -175,7 +175,7 @@ func promptInstall(writer io.Writer, profileName string) bool {
func promptProfileName(writer io.Writer) string {
answer := ""
fmt.Fprintf(writer, "\nPlease select higress install configration profile:\n")
fmt.Fprintf(writer, "\nPlease select higress install configuration profile:\n")
fmt.Fprintf(writer, "\n1.Install higress to local kubernetes cluster like kind etc.\n")
fmt.Fprintf(writer, "\n2.Install higress to kubernetes cluster\n")
fmt.Fprintf(writer, "\n3.Install higress to local docker environment\n")

View File

@@ -176,7 +176,7 @@ func (a *Agent) checkSudoPermission() error {
case <-time.After(5 * time.Second):
cmd2.Process.Signal(os.Interrupt)
if !a.quiet {
fmt.Fprintf(a.writer, "checked result: timeout execeed and need sudo with password\n")
fmt.Fprintf(a.writer, "checked result: timeout exceed and need sudo with password\n")
}
a.runSudoState = SudoWithPassword

View File

@@ -108,7 +108,7 @@ func upgrade(writer io.Writer, iArgs *InstallArgs) error {
func promptUpgrade(writer io.Writer) bool {
answer := ""
for {
fmt.Fprintf(writer, "All Higress resources will be upgraed from the cluster. \nProceed? (y/N)")
fmt.Fprintf(writer, "All Higress resources will be upgrade from the cluster. \nProceed? (y/N)")
fmt.Scanln(&answer)
if strings.TrimSpace(answer) == "y" {
fmt.Fprintf(writer, "\n")
@@ -170,7 +170,7 @@ func promptProfileContexts(writer io.Writer, profileContexts []*installer.Profil
if len(profileContexts) == 1 {
fmt.Fprintf(writer, "\nFound a profile:: ")
} else {
fmt.Fprintf(writer, "\nPlease select higress installed configration profiles:\n")
fmt.Fprintf(writer, "\nPlease select higress installed configuration profiles:\n")
}
index := 1
for _, profileContext := range profileContexts {

View File

@@ -32,7 +32,7 @@ func ParseProtocol(s string) Protocol {
return TCP
case "http":
return HTTP
case "grpc":
case "grpc", "triple", "tri":
return GRPC
case "dubbo":
return Dubbo

View File

@@ -51,6 +51,7 @@ import (
higressv1 "github.com/alibaba/higress/api/networking/v1"
extlisterv1 "github.com/alibaba/higress/client/pkg/listers/extensions/v1alpha1"
netlisterv1 "github.com/alibaba/higress/client/pkg/listers/networking/v1"
"github.com/alibaba/higress/pkg/cert"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
"github.com/alibaba/higress/pkg/ingress/kube/common"
"github.com/alibaba/higress/pkg/ingress/kube/configmap"
@@ -144,6 +145,8 @@ type IngressConfig struct {
namespace string
clusterId string
httpsConfigMgr *cert.ConfigMgr
}
func NewIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater, namespace, clusterId string) *IngressConfig {
@@ -180,6 +183,9 @@ func NewIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater,
higressConfigController := configmap.NewController(localKubeClient, clusterId, namespace)
config.configmapMgr = configmap.NewConfigmapMgr(XDSUpdater, namespace, higressConfigController, higressConfigController.Lister())
httpsConfigMgr, _ := cert.NewConfigMgr(namespace, localKubeClient)
config.httpsConfigMgr = httpsConfigMgr
return config
}
@@ -347,6 +353,10 @@ func (m *IngressConfig) convertGateways(configs []common.WrapperConfig) []config
Gateways: map[string]*common.WrapperGateway{},
}
httpsCredentialConfig, err := m.httpsConfigMgr.GetConfigFromConfigmap()
if err != nil {
IngressLog.Errorf("Get higress https configmap err %v", err)
}
for idx := range configs {
cfg := configs[idx]
clusterId := common.GetClusterId(cfg.Config.Annotations)
@@ -356,7 +366,7 @@ func (m *IngressConfig) convertGateways(configs []common.WrapperConfig) []config
if ingressController == nil {
continue
}
if err := ingressController.ConvertGateway(&convertOptions, &cfg); err != nil {
if err := ingressController.ConvertGateway(&convertOptions, &cfg, httpsCredentialConfig); err != nil {
IngressLog.Errorf("Convert ingress %s/%s to gateway fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
}
}
@@ -831,6 +841,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
StructValue: rule.Config,
}
var matchItems []*types.Value
// match ingress
for _, ing := range rule.Ingress {
matchItems = append(matchItems, &types.Value{
Kind: &types.Value_StringValue{
@@ -851,6 +862,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
})
continue
}
// match domain
for _, domain := range rule.Domain {
matchItems = append(matchItems, &types.Value{
Kind: &types.Value_StringValue{
@@ -858,10 +870,31 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
},
})
}
if len(matchItems) > 0 {
v.StructValue.Fields["_match_domain_"] = &types.Value{
Kind: &types.Value_ListValue{
ListValue: &types.ListValue{
Values: matchItems,
},
},
}
ruleValues = append(ruleValues, &types.Value{
Kind: v,
})
continue
}
// match service
for _, service := range rule.Service {
matchItems = append(matchItems, &types.Value{
Kind: &types.Value_StringValue{
StringValue: service,
},
})
}
if len(matchItems) == 0 {
return nil, fmt.Errorf("invalid match rule has no match condition, rule:%v", rule)
}
v.StructValue.Fields["_match_domain_"] = &types.Value{
v.StructValue.Fields["_match_service_"] = &types.Value{
Kind: &types.Value_ListValue{
ListValue: &types.ListValue{
Values: matchItems,
@@ -908,7 +941,7 @@ func (m *IngressConfig) AddOrUpdateWasmPlugin(clusterNamespacedName util.Cluster
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.wasmPluginHandlers {
IngressLog.Debug("WasmPlugin triggerd update")
IngressLog.Debug("WasmPlugin triggered update")
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, model.EventUpdate)
}
istioWasmPlugin, err := m.convertIstioWasmPlugin(&wasmPlugin.Spec)
@@ -950,7 +983,7 @@ func (m *IngressConfig) DeleteWasmPlugin(clusterNamespacedName util.ClusterNames
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.wasmPluginHandlers {
IngressLog.Debug("WasmPlugin triggerd update")
IngressLog.Debug("WasmPlugin triggered update")
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, model.EventDelete)
}
}
@@ -977,7 +1010,7 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.serviceEntryHandlers {
IngressLog.Debug("McpBridge triggerd serviceEntry update")
IngressLog.Debug("McpBridge triggered serviceEntry update")
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, model.EventUpdate)
}
}, m.localKubeClient, m.namespace)
@@ -1032,7 +1065,7 @@ func (m *IngressConfig) AddOrUpdateHttp2Rpc(clusterNamespacedName util.ClusterNa
}
func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespacedName) {
IngressLog.Infof("Http2Rpc triggerd deleted event %s", clusterNamespacedName.Name)
IngressLog.Infof("Http2Rpc triggered deleted event %s", clusterNamespacedName.Name)
if clusterNamespacedName.Namespace != m.namespace {
return
}
@@ -1044,7 +1077,7 @@ func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespa
}
m.mutex.Unlock()
if hit {
IngressLog.Infof("Http2Rpc triggerd deleted event executed %s", clusterNamespacedName.Name)
IngressLog.Infof("Http2Rpc triggered deleted event executed %s", clusterNamespacedName.Name)
push := func(kind config.GroupVersionKind) {
m.XDSUpdater.ConfigUpdate(&model.PushRequest{
Full: true,
@@ -1150,13 +1183,13 @@ func (m *IngressConfig) constructHttp2RpcEnvoyFilter(http2rpcConfig *annotations
IngressLog.Infof("Found http2rpc mappings %v", mappings)
if _, exist := mappings[http2rpcConfig.Name]; !exist {
IngressLog.Errorf("Http2RpcConfig name %s, not found Http2Rpc CRD", http2rpcConfig.Name)
return nil, errors.New("invalid http2rpcConfig has no useable http2rpc")
return nil, errors.New("invalid http2rpcConfig has no usable http2rpc")
}
http2rpcCRD := mappings[http2rpcConfig.Name]
if http2rpcCRD.GetDubbo() == nil {
IngressLog.Errorf("Http2RpcConfig name %s, only support Http2Rpc CRD Dubbo Service type", http2rpcConfig.Name)
return nil, errors.New("invalid http2rpcConfig has no useable http2rpc")
return nil, errors.New("invalid http2rpcConfig has no usable http2rpc")
}
httpRoute := route.HTTPRoute
@@ -1283,7 +1316,7 @@ func (m *IngressConfig) constructHttp2RpcMethods(dubbo *higressv1.DubboService)
var method = make(map[string]interface{})
method["name"] = serviceMethod.GetServiceMethod()
var params []interface{}
// paramFromEntireBody is for methods with single parameter. So when paramFromEntireBody exists, we just ignore parmas.
// paramFromEntireBody is for methods with single parameter. So when paramFromEntireBody exists, we just ignore params.
var paramFromEntireBody = serviceMethod.GetParamFromEntireBody()
if paramFromEntireBody != nil {
var param = make(map[string]interface{})

View File

@@ -17,14 +17,14 @@ package common
import (
"strings"
"github.com/alibaba/higress/pkg/cert"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
gatewaytool "istio.io/istio/pkg/config/gateway"
listerv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
)
type ServiceKey struct {
@@ -121,7 +121,7 @@ type IngressController interface {
SecretLister() listerv1.SecretLister
ConvertGateway(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
ConvertGateway(convertOptions *ConvertOptions, wrapper *WrapperConfig, httpsCredentialConfig *cert.Config) error
ConvertHTTPRoute(convertOptions *ConvertOptions, wrapper *WrapperConfig) error

View File

@@ -55,6 +55,7 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/secret"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
var (
@@ -87,8 +88,6 @@ type controller struct {
secretController secret.SecretController
statusSyncer *statusSyncer
configMgr *cert.ConfigMgr
}
// NewController creates a new Kubernetes controller
@@ -107,7 +106,6 @@ func NewController(localKubeClient, client kubeclient.Client, options common.Opt
IngressLog.Infof("Skipping IngressClass, resource not supported for cluster %s", options.ClusterId)
}
configMgr, _ := cert.NewConfigMgr(options.SystemNamespace, client.Kube())
c := &controller{
options: options,
queue: q,
@@ -118,7 +116,6 @@ func NewController(localKubeClient, client kubeclient.Client, options common.Opt
serviceInformer: serviceInformer.Informer(),
serviceLister: serviceInformer.Lister(),
secretController: secretController,
configMgr: configMgr,
}
handler := controllers.LatestVersionHandlerFuncs(controllers.EnqueueForSelf(q))
@@ -354,7 +351,7 @@ func extractTLSSecretName(host string, tls []ingress.IngressTLS) string {
return ""
}
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig, httpsCredentialConfig *cert.Config) error {
if convertOptions == nil {
return fmt.Errorf("convertOptions is nil")
}
@@ -377,7 +374,6 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, either `defaultBackend` or `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
}
httpsCredentialConfig, _ := c.configMgr.GetConfigFromConfigmap()
for _, rule := range ingressV1Beta.Rules {
// Need create builder for every rule.
domainBuilder := &common.IngressDomainBuilder{
@@ -429,10 +425,36 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
// Get tls secret matching the rule host
secretName := extractTLSSecretName(rule.Host, ingressV1Beta.TLS)
secretNamespace := cfg.Namespace
// If there is no matching secret, try to get it from configmap.
if secretName == "" && httpsCredentialConfig != nil {
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
secretNamespace = c.options.SystemNamespace
if secretName != "" {
if httpsCredentialConfig != nil && httpsCredentialConfig.FallbackForInvalidSecret {
_, err := c.secretController.Lister().Secrets(secretNamespace).Get(secretName)
if err != nil {
if k8serrors.IsNotFound(err) {
// If there is no matching secret, try to get it from configmap.
matchSecretName := httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
if matchSecretName != "" {
namespace, secret := cert.ParseTLSSecret(matchSecretName)
if namespace == "" {
secretNamespace = c.options.SystemNamespace
} else {
secretNamespace = namespace
}
secretName = secret
}
}
}
}
} else {
// If there is no matching secret, try to get it from configmap.
if httpsCredentialConfig != nil {
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
secretNamespace = c.options.SystemNamespace
namespace, secret := cert.ParseTLSSecret(secretName)
if namespace != "" {
secretNamespace = namespace
secretName = secret
}
}
}
if secretName == "" {
// There no matching secret, so just skip.

View File

@@ -334,7 +334,7 @@ func testConvertGateway(t *testing.T, c common.IngressController) {
}
for _, testcase := range testcases {
err := c.ConvertGateway(testcase.input.options, testcase.input.wrapperConfig)
err := c.ConvertGateway(testcase.input.options, testcase.input.wrapperConfig, nil)
if err != nil {
require.Equal(t, testcase.expectNoError, false)
} else {

View File

@@ -54,6 +54,7 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/secret"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
var (
@@ -85,8 +86,6 @@ type controller struct {
secretController secret.SecretController
statusSyncer *statusSyncer
configMgr *cert.ConfigMgr
}
// NewController creates a new Kubernetes controller
@@ -99,7 +98,6 @@ func NewController(localKubeClient, client kubeclient.Client, options common.Opt
classes := client.KubeInformer().Networking().V1().IngressClasses()
classes.Informer()
configMgr, _ := cert.NewConfigMgr(options.SystemNamespace, client.Kube())
c := &controller{
options: options,
queue: q,
@@ -110,7 +108,6 @@ func NewController(localKubeClient, client kubeclient.Client, options common.Opt
serviceInformer: serviceInformer.Informer(),
serviceLister: serviceInformer.Lister(),
secretController: secretController,
configMgr: configMgr,
}
handler := controllers.LatestVersionHandlerFuncs(controllers.EnqueueForSelf(q))
@@ -346,7 +343,7 @@ func extractTLSSecretName(host string, tls []ingress.IngressTLS) string {
return ""
}
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig, httpsCredentialConfig *cert.Config) error {
// Ignore canary config.
if wrapper.AnnotationsConfig.IsCanary() {
return nil
@@ -363,7 +360,6 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, either `defaultBackend` or `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
}
httpsCredentialConfig, _ := c.configMgr.GetConfigFromConfigmap()
for _, rule := range ingressV1.Rules {
// Need create builder for every rule.
domainBuilder := &common.IngressDomainBuilder{
@@ -415,11 +411,38 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
// Get tls secret matching the rule host
secretName := extractTLSSecretName(rule.Host, ingressV1.TLS)
secretNamespace := cfg.Namespace
// If there is no matching secret, try to get it from configmap.
if secretName == "" && httpsCredentialConfig != nil {
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
secretNamespace = c.options.SystemNamespace
if secretName != "" {
if httpsCredentialConfig != nil && httpsCredentialConfig.FallbackForInvalidSecret {
_, err := c.secretController.Lister().Secrets(secretNamespace).Get(secretName)
if err != nil {
if k8serrors.IsNotFound(err) {
// If there is no matching secret, try to get it from configmap.
matchSecretName := httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
if matchSecretName != "" {
namespace, secret := cert.ParseTLSSecret(matchSecretName)
if namespace == "" {
secretNamespace = c.options.SystemNamespace
} else {
secretNamespace = namespace
}
secretName = secret
}
}
}
}
} else {
// If there is no matching secret, try to get it from configmap.
if httpsCredentialConfig != nil {
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
secretNamespace = c.options.SystemNamespace
namespace, secret := cert.ParseTLSSecret(secretName)
if namespace != "" {
secretNamespace = namespace
secretName = secret
}
}
}
if secretName == "" {
// There no matching secret, so just skip.
continue

View File

@@ -163,7 +163,6 @@ func (c *controller) processNextWorkItem() bool {
func (c *controller) onEvent(namespacedName types.NamespacedName) error {
event := model.EventUpdate
ing, err := c.ingressLister.Ingresses(namespacedName.Namespace).Get(namespacedName.Name)
ing.Status.InitializeConditions()
if err != nil {
if kerrors.IsNotFound(err) {
event = model.EventDelete
@@ -181,6 +180,8 @@ func (c *controller) onEvent(namespacedName types.NamespacedName) error {
return nil
}
ing.Status.InitializeConditions()
// we should check need process only when event is not delete,
// if it is delete event, and previously processed, we need to process too.
if event != model.EventDelete {

View File

@@ -0,0 +1,53 @@
## 介绍
此 SDK 用于使用 AssemblyScript 语言开发 Higress 的 Wasm 插件。
### 如何使用SDK
创建一个新的 AssemblyScript 项目。
```
npm init
npm install --save-dev assemblyscript
npx asinit .
```
在asconfig.json文件中作为传递给asc编译器的选项之一包含"use": "abort=abort_proc_exit"。
```
{
"options": {
"use": "abort=abort_proc_exit"
}
}
```
`"@higress/wasm-assemblyscript": "^0.0.4"`添加到你的依赖项中,然后运行`npm install`
### 本地构建
```
npm run asbuild
```
构建结果将在`build`文件夹中。其中,`debug.wasm``release.wasm`是已编译的文件,在生产环境中建议使用`release.wasm`
注:如果需要插件带有 name section 信息需要带上`"debug": true`,编译参数解释详见[using-the-compiler](https://www.assemblyscript.org/compiler.html#using-the-compiler)。
```json
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false,
"debug": true
}
```
### AssemblyScript 限制
此 SDK 使用的 AssemblyScript 版本为`0.27.29`,参考[AssemblyScript Status](https://www.assemblyscript.org/status.html)该版本尚未支持闭包、异常、迭代器等特性并且JSON正则表达式等功能还尚未在标准库中实现暂时需要使用社区提供的实现。

View File

@@ -0,0 +1,23 @@
{
"targets": {
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false
}
},
"options": {
"bindings": "esm",
"use": "abort=abort_proc_exit"
}
}

View File

@@ -0,0 +1,214 @@
import {
log,
LogLevelValues,
get_property,
WasmResultValues,
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
import { getRequestHost } from "./request_wrapper";
export abstract class Cluster {
abstract clusterName(): string;
abstract hostName(): string;
}
export class RouteCluster extends Cluster {
host: string;
constructor(host: string = "") {
super();
this.host = host;
}
clusterName(): string {
let result = get_property("cluster_name");
if (result.status != WasmResultValues.Ok) {
log(LogLevelValues.error, "get route cluster failed");
return "";
}
return String.UTF8.decode(result.returnValue);
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return getRequestHost();
}
}
export class K8sCluster extends Cluster {
serviceName: string;
namespace: string;
port: i64;
version: string;
host: string;
constructor(
serviceName: string,
namespace: string,
port: i64,
version: string = "",
host: string = ""
) {
super();
this.serviceName = serviceName;
this.namespace = namespace;
this.port = port;
this.version = version;
this.host = host;
}
clusterName(): string {
let namespace = this.namespace != "" ? this.namespace : "default";
return `outbound|${this.port}|${this.version}|${this.serviceName}.${namespace}.svc.cluster.local`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return `${this.serviceName}.${this.namespace}.svc.cluster.local`;
}
}
export class NacosCluster extends Cluster {
serviceName: string;
group: string;
namespaceID: string;
port: i64;
isExtRegistry: boolean;
version: string;
host: string;
constructor(
serviceName: string,
namespaceID: string,
port: i64,
// use DEFAULT-GROUP by default
group: string = "DEFAULT-GROUP",
// set true if use edas/sae registry
isExtRegistry: boolean = false,
version: string = "",
host: string = ""
) {
super();
this.serviceName = serviceName;
this.group = group.replace("_", "-");
this.namespaceID = namespaceID;
this.port = port;
this.isExtRegistry = isExtRegistry;
this.version = version;
this.host = host;
}
clusterName(): string {
let tail = "nacos" + (this.isExtRegistry ? "-ext" : "");
return `outbound|${this.port}|${this.version}|${this.serviceName}.${this.group}.${this.namespaceID}.${tail}`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.serviceName;
}
}
export class StaticIpCluster extends Cluster {
serviceName: string;
port: i64;
host: string;
constructor(serviceName: string, port: i64, host: string = "") {
super()
this.serviceName = serviceName;
this.port = port;
this.host = host;
}
clusterName(): string {
return `outbound|${this.port}||${this.serviceName}.static`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.serviceName;
}
}
export class DnsCluster extends Cluster {
serviceName: string;
domain: string;
port: i64;
constructor(serviceName: string, domain: string, port: i64) {
super();
this.serviceName = serviceName;
this.domain = domain;
this.port = port;
}
clusterName(): string {
return `outbound|${this.port}||${this.serviceName}.dns`;
}
hostName(): string {
return this.domain;
}
}
export class ConsulCluster extends Cluster {
serviceName: string;
datacenter: string;
port: i64;
host: string;
constructor(
serviceName: string,
datacenter: string,
port: i64,
host: string = ""
) {
super();
this.serviceName = serviceName;
this.datacenter = datacenter;
this.port = port;
this.host = host;
}
clusterName(): string {
return `outbound|${this.port}||${this.serviceName}.${this.datacenter}.consul`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.serviceName;
}
}
export class FQDNCluster extends Cluster {
fqdn: string;
host: string;
port: i64;
constructor(fqdn: string, port: i64, host: string = "") {
super();
this.fqdn = fqdn;
this.host = host;
this.port = port;
}
clusterName(): string {
return `outbound|${this.port}||${this.fqdn}`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.fqdn;
}
}

View File

@@ -0,0 +1,120 @@
import {
Cluster
} from "./cluster_wrapper"
import {
log,
LogLevelValues,
Headers,
HeaderPair,
root_context,
BufferTypeValues,
get_buffer_bytes,
BaseContext,
stream_context,
WasmResultValues,
RootContext,
ResponseCallBack
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
export interface HttpClient {
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
}
const methodArrayBuffer: ArrayBuffer = String.UTF8.encode(":method");
const pathArrayBuffer: ArrayBuffer = String.UTF8.encode(":path");
const authorityArrayBuffer: ArrayBuffer = String.UTF8.encode(":authority");
const StatusBadGateway: i32 = 502;
export class ClusterClient {
cluster: Cluster;
constructor(cluster: Cluster) {
this.cluster = cluster;
}
private httpCall(method: string, path: string, headers: Headers, body: ArrayBuffer, callback: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
if (root_context == null) {
log(LogLevelValues.error, "Root context is null");
return false;
}
for (let i: i32 = headers.length - 1; i >= 0; i--) {
const key = String.UTF8.decode(headers[i].key)
if ((key == ":method") || (key == ":path") || (key == ":authority")) {
headers.splice(i, 1);
}
}
headers.push(new HeaderPair(methodArrayBuffer, String.UTF8.encode(method)));
headers.push(new HeaderPair(pathArrayBuffer, String.UTF8.encode(path)));
headers.push(new HeaderPair(authorityArrayBuffer, String.UTF8.encode(this.cluster.hostName())));
const result = (root_context as RootContext).httpCall(this.cluster.clusterName(), headers, body, [], timeoutMillisecond, root_context as BaseContext, callback,
(_origin_context: BaseContext, _numHeaders: u32, body_size: usize, _trailers: u32, callback: ResponseCallBack): void => {
const respBody = get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, 0, body_size as u32);
const respHeaders = stream_context.headers.http_callback.get_headers()
let code = StatusBadGateway;
let headers = new Array<HeaderPair>();
for (let i = 0; i < respHeaders.length; i++) {
const h = respHeaders[i];
if (String.UTF8.decode(h.key) == ":status") {
code = <i32>parseInt(String.UTF8.decode(h.value))
}
headers.push(new HeaderPair(h.key, h.value));
}
log(LogLevelValues.debug, `http call end, code: ${code}, body: ${String.UTF8.decode(respBody)}`)
callback(code, headers, respBody);
})
log(LogLevelValues.debug, `http call start, cluster: ${this.cluster.clusterName()}, method: ${method}, path: ${path}, body: ${String.UTF8.decode(body)}, timeout: ${timeoutMillisecond}`)
if (result != WasmResultValues.Ok) {
log(LogLevelValues.error, `http call failed, result: ${result}`)
return false
}
return true
}
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("GET", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
}
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("HEAD", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
}
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("OPTIONS", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
}
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("POST", path, headers, body, cb, timeoutMillisecond);
}
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("PUT", path, headers, body, cb, timeoutMillisecond);
}
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("PATCH", path, headers, body, cb, timeoutMillisecond);
}
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("DELETE", path, headers, body, cb, timeoutMillisecond);
}
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("CONNECT", path, headers, body, cb, timeoutMillisecond);
}
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("TRACE", path, headers, body, cb, timeoutMillisecond);
}
}

View File

@@ -0,0 +1,18 @@
export {RouteCluster,
K8sCluster,
NacosCluster,
ConsulCluster,
FQDNCluster,
StaticIpCluster} from "./cluster_wrapper"
export {HttpClient,
ClusterClient} from "./http_wrapper"
export {Log} from "./log_wrapper"
export {SetCtx,
HttpContext,
ParseConfigBy,
ProcessRequestBodyBy,
ProcessRequestHeadersBy,
ProcessResponseBodyBy,
ProcessResponseHeadersBy,
Logger, RegisteTickFunc} from "./plugin_wrapper"
export {ParseResult} from "./rule_matcher"

View File

@@ -0,0 +1,66 @@
import { log, LogLevelValues } from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
enum LogLevel {
Trace = 0,
Debug,
Info,
Warn,
Error,
Critical,
}
export class Log {
private pluginName: string;
constructor(pluginName: string) {
this.pluginName = pluginName;
}
private log(level: LogLevel, msg: string): void {
let formattedMsg = `[${this.pluginName}] ${msg}`;
switch (level) {
case LogLevel.Trace:
log(LogLevelValues.trace, formattedMsg);
break;
case LogLevel.Debug:
log(LogLevelValues.debug, formattedMsg);
break;
case LogLevel.Info:
log(LogLevelValues.info, formattedMsg);
break;
case LogLevel.Warn:
log(LogLevelValues.warn, formattedMsg);
break;
case LogLevel.Error:
log(LogLevelValues.error, formattedMsg);
break;
case LogLevel.Critical:
log(LogLevelValues.critical, formattedMsg);
break;
}
}
public Trace(msg: string): void {
this.log(LogLevel.Trace, msg);
}
public Debug(msg: string): void {
this.log(LogLevel.Debug, msg);
}
public Info(msg: string): void {
this.log(LogLevel.Info, msg);
}
public Warn(msg: string): void {
this.log(LogLevel.Warn, msg);
}
public Error(msg: string): void {
this.log(LogLevel.Error, msg);
}
public Critical(msg: string): void {
this.log(LogLevel.Critical, msg);
}
}

View File

@@ -0,0 +1,445 @@
import { Log } from "./log_wrapper";
import {
Context,
FilterHeadersStatusValues,
RootContext,
setRootContext,
proxy_set_effective_context,
log,
LogLevelValues,
FilterDataStatusValues,
get_buffer_bytes,
BufferTypeValues,
set_tick_period_milliseconds,
get_current_time_nanoseconds
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
import {
getRequestHost,
getRequestMethod,
getRequestPath,
getRequestScheme,
isBinaryRequestBody,
} from "./request_wrapper";
import { RuleMatcher, ParseResult } from "./rule_matcher";
import { JSON } from "assemblyscript-json/assembly";
export function SetCtx<PluginConfig>(
pluginName: string,
setFuncs: usize[] = []
): void {
const rootContextId = 1
setRootContext(new CommonRootCtx<PluginConfig>(rootContextId, pluginName, setFuncs));
}
export interface HttpContext {
Scheme(): string;
Host(): string;
Path(): string;
Method(): string;
SetContext(key: string, value: usize): void;
GetContext(key: string): usize;
DontReadRequestBody(): void;
DontReadResponseBody(): void;
}
type ParseConfigFunc<PluginConfig> = (
json: JSON.Obj,
) => ParseResult<PluginConfig>;
type OnHttpHeadersFunc<PluginConfig> = (
context: HttpContext,
config: PluginConfig,
) => FilterHeadersStatusValues;
type OnHttpBodyFunc<PluginConfig> = (
context: HttpContext,
config: PluginConfig,
body: ArrayBuffer,
) => FilterDataStatusValues;
export var Logger: Log = new Log("");
class CommonRootCtx<PluginConfig> extends RootContext {
pluginName: string;
hasCustomConfig: boolean;
ruleMatcher: RuleMatcher<PluginConfig>;
parseConfig: ParseConfigFunc<PluginConfig> | null;
onHttpRequestHeaders: OnHttpHeadersFunc<PluginConfig> | null;
onHttpRequestBody: OnHttpBodyFunc<PluginConfig> | null;
onHttpResponseHeaders: OnHttpHeadersFunc<PluginConfig> | null;
onHttpResponseBody: OnHttpBodyFunc<PluginConfig> | null;
onTickFuncs: Array<TickFuncEntry>;
constructor(context_id: u32, pluginName: string, setFuncs: usize[]) {
super(context_id);
this.pluginName = pluginName;
Logger = new Log(pluginName);
this.hasCustomConfig = true;
this.onHttpRequestHeaders = null;
this.onHttpRequestBody = null;
this.onHttpResponseHeaders = null;
this.onHttpResponseBody = null;
this.parseConfig = null;
this.ruleMatcher = new RuleMatcher<PluginConfig>();
this.onTickFuncs = new Array<TickFuncEntry>();
for (let i = 0; i < setFuncs.length; i++) {
changetype<Closure<PluginConfig>>(setFuncs[i]).lambdaFn(
setFuncs[i],
this
);
}
if (this.parseConfig == null) {
this.hasCustomConfig = false;
this.parseConfig = (json: JSON.Obj): ParseResult<PluginConfig> =>{ return new ParseResult<PluginConfig>(null, true); };
}
}
createContext(context_id: u32): Context {
return new CommonCtx<PluginConfig>(context_id, this);
}
onConfigure(configuration_size: u32): boolean {
super.onConfigure(configuration_size);
const data = this.getConfiguration();
let jsonData: JSON.Obj = new JSON.Obj();
if (data == "{}") {
if (this.hasCustomConfig) {
log(LogLevelValues.warn, "config is empty, but has ParseConfigFunc");
}
} else {
const parseData = JSON.parse(data);
if (parseData.isObj) {
jsonData = changetype<JSON.Obj>(JSON.parse(data));
} else {
log(LogLevelValues.error, "parse json data failed")
return false;
}
}
if (!this.ruleMatcher.parseRuleConfig(jsonData, this.parseConfig as ParseConfigFunc<PluginConfig>)) {
return false;
}
if (globalOnTickFuncs.length > 0) {
this.onTickFuncs = globalOnTickFuncs;
set_tick_period_milliseconds(100);
}
return true;
}
onTick(): void {
for (let i = 0; i < this.onTickFuncs.length; i++) {
const tickFuncEntry = this.onTickFuncs[i];
const now = getCurrentTimeMilliseconds();
if (tickFuncEntry.lastExecuted + tickFuncEntry.tickPeriod <= now) {
tickFuncEntry.tickFunc();
tickFuncEntry.lastExecuted = getCurrentTimeMilliseconds();
}
}
}
}
function getCurrentTimeMilliseconds(): u64 {
return get_current_time_nanoseconds() / 1000000;
}
class TickFuncEntry {
lastExecuted: u64;
tickPeriod: u64;
tickFunc: () => void;
constructor(lastExecuted: u64, tickPeriod: u64, tickFunc: () => void) {
this.lastExecuted = lastExecuted;
this.tickPeriod = tickPeriod;
this.tickFunc = tickFunc;
}
}
var globalOnTickFuncs = new Array<TickFuncEntry>();
export function RegisteTickFunc(tickPeriod: i64, tickFunc: () => void): void {
globalOnTickFuncs.push(new TickFuncEntry(0, tickPeriod, tickFunc));
}
class Closure<PluginConfig> {
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void;
parseConfigFunc: ParseConfigFunc<PluginConfig> | null;
onHttpHeadersFunc: OnHttpHeadersFunc<PluginConfig> | null;
OnHttpBodyFunc: OnHttpBodyFunc<PluginConfig> | null;
constructor(
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void
) {
this.lambdaFn = lambdaFn;
this.parseConfigFunc = null;
this.onHttpHeadersFunc = null;
this.OnHttpBodyFunc = null;
}
setParseConfigFunc(f: ParseConfigFunc<PluginConfig>): void {
this.parseConfigFunc = f;
}
setHttpHeadersFunc(f: OnHttpHeadersFunc<PluginConfig>): void {
this.onHttpHeadersFunc = f;
}
setHttpBodyFunc(f: OnHttpBodyFunc<PluginConfig>): void {
this.OnHttpBodyFunc = f;
}
}
export function ParseConfigBy<PluginConfig>(
f: ParseConfigFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).parseConfigFunc;
if (f != null) {
ctx.parseConfig = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setParseConfigFunc(f);
return changetype<usize>(closure);
}
export function ProcessRequestHeadersBy<PluginConfig>(
f: OnHttpHeadersFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
if (f != null) {
ctx.onHttpRequestHeaders = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpHeadersFunc(f);
return changetype<usize>(closure);
}
export function ProcessRequestBodyBy<PluginConfig>(
f: OnHttpBodyFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
if (f != null) {
ctx.onHttpRequestBody = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpBodyFunc(f);
return changetype<usize>(closure);
}
export function ProcessResponseHeadersBy<PluginConfig>(
f: OnHttpHeadersFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
if (f != null) {
ctx.onHttpResponseHeaders = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpHeadersFunc(f);
return changetype<usize>(closure);
}
export function ProcessResponseBodyBy<PluginConfig>(
f: OnHttpBodyFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
if (f != null) {
ctx.onHttpResponseBody = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpBodyFunc(f);
return changetype<usize>(closure);
}
class CommonCtx<PluginConfig> extends Context implements HttpContext {
commonRootCtx: CommonRootCtx<PluginConfig>;
config: PluginConfig |null;
needRequestBody: boolean;
needResponseBody: boolean;
requestBodySize: u32;
responseBodySize: u32;
contextID: u32;
userContext: Map<string, usize>;
constructor(context_id: u32, root_context: CommonRootCtx<PluginConfig>) {
super(context_id, root_context);
this.userContext = new Map<string, usize>();
this.commonRootCtx = root_context;
this.contextID = context_id;
this.requestBodySize = 0;
this.responseBodySize = 0;
this.config = null
if (this.commonRootCtx.onHttpRequestHeaders != null) {
this.needResponseBody = true;
} else {
this.needResponseBody = false;
}
if (this.commonRootCtx.onHttpRequestBody != null) {
this.needRequestBody = true;
} else {
this.needRequestBody = false;
}
}
SetContext(key: string, value: usize): void {
this.userContext.set(key, value);
}
GetContext(key: string): usize {
return this.userContext.get(key);
}
Scheme(): string {
proxy_set_effective_context(this.contextID);
return getRequestScheme();
}
Host(): string {
proxy_set_effective_context(this.contextID);
return getRequestHost();
}
Path(): string {
proxy_set_effective_context(this.contextID);
return getRequestPath();
}
Method(): string {
proxy_set_effective_context(this.contextID);
return getRequestMethod();
}
DontReadRequestBody(): void {
this.needRequestBody = false;
}
DontReadResponseBody(): void {
this.needResponseBody = false;
}
onRequestHeaders(_a: u32, _end_of_stream: boolean): FilterHeadersStatusValues {
const parseResult = this.commonRootCtx.ruleMatcher.getMatchConfig();
if (parseResult.success == false) {
log(LogLevelValues.error, "get match config failed");
return FilterHeadersStatusValues.Continue;
}
this.config = parseResult.pluginConfig;
if (isBinaryRequestBody()) {
this.needRequestBody = false;
}
if (this.commonRootCtx.onHttpRequestHeaders == null) {
return FilterHeadersStatusValues.Continue;
}
return this.commonRootCtx.onHttpRequestHeaders(
this,
this.config as PluginConfig
);
}
onRequestBody(
body_buffer_length: usize,
end_of_stream: boolean
): FilterDataStatusValues {
if (this.config == null || !this.needRequestBody) {
return FilterDataStatusValues.Continue;
}
if (this.commonRootCtx.onHttpRequestBody == null) {
return FilterDataStatusValues.Continue;
}
this.requestBodySize += body_buffer_length as u32;
if (!end_of_stream) {
return FilterDataStatusValues.StopIterationAndBuffer;
}
const body = get_buffer_bytes(
BufferTypeValues.HttpRequestBody,
0,
this.requestBodySize
);
return this.commonRootCtx.onHttpRequestBody(
this,
this.config as PluginConfig,
body
);
}
onResponseHeaders(_a: u32, _end_of_stream: bool): FilterHeadersStatusValues {
if (this.config == null) {
return FilterHeadersStatusValues.Continue;
}
if (isBinaryRequestBody()) {
this.needResponseBody = false;
}
if (this.commonRootCtx.onHttpResponseHeaders == null) {
return FilterHeadersStatusValues.Continue;
}
return this.commonRootCtx.onHttpResponseHeaders(
this,
this.config as PluginConfig
);
}
onResponseBody(
body_buffer_length: usize,
end_of_stream: bool
): FilterDataStatusValues {
if (this.config == null) {
return FilterDataStatusValues.Continue;
}
if (this.commonRootCtx.onHttpResponseBody == null) {
return FilterDataStatusValues.Continue;
}
if (!this.needResponseBody) {
return FilterDataStatusValues.Continue;
}
this.responseBodySize += body_buffer_length as u32;
if (!end_of_stream) {
return FilterDataStatusValues.StopIterationAndBuffer;
}
const body = get_buffer_bytes(
BufferTypeValues.HttpResponseBody,
0,
this.responseBodySize
);
return this.commonRootCtx.onHttpResponseBody(
this,
this.config as PluginConfig,
body
);
}
}

View File

@@ -0,0 +1,65 @@
import {
stream_context,
log,
LogLevelValues
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
export function getRequestScheme(): string {
let scheme: string = stream_context.headers.request.get(":scheme");
if (scheme == "") {
log(LogLevelValues.error, "Parse request scheme failed");
}
return scheme;
}
export function getRequestHost(): string {
let host: string = stream_context.headers.request.get(":authority");
if (host == "") {
log(LogLevelValues.error, "Parse request host failed");
}
return host;
}
export function getRequestPath(): string {
let path: string = stream_context.headers.request.get(":path");
if (path == "") {
log(LogLevelValues.error, "Parse request path failed");
}
return path;
}
export function getRequestMethod(): string {
let method: string = stream_context.headers.request.get(":method");
if (method == "") {
log(LogLevelValues.error, "Parse request method failed");
}
return method;
}
export function isBinaryRequestBody(): boolean {
let contentType: string = stream_context.headers.request.get("content-type");
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
return true;
}
let encoding: string = stream_context.headers.request.get("content-encoding");
if (encoding != "") {
return true;
}
return false;
}
export function isBinaryResponseBody(): boolean {
let contentType: string = stream_context.headers.response.get("content-type");
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
return true;
}
let encoding: string = stream_context.headers.response.get("content-encoding");
if (encoding != "") {
return true;
}
return false;
}

View File

@@ -0,0 +1,346 @@
import { getRequestHost } from "./request_wrapper";
import {
get_property,
LogLevelValues,
log,
WasmResultValues,
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
import { JSON } from "assemblyscript-json/assembly";
enum Category {
Route,
Host,
RoutePrefix,
Service
}
enum MatchType {
Prefix,
Exact,
Suffix,
}
const RULES_KEY: string = "_rules_";
const MATCH_ROUTE_KEY: string = "_match_route_";
const MATCH_DOMAIN_KEY: string = "_match_domain_";
const MATCH_SERVICE_KEY: string = "_match_service_";
const MATCH_ROUTE_PREFIX_KEY: string = "_match_route_prefix_"
class HostMatcher {
matchType: MatchType;
host: string;
constructor(matchType: MatchType, host: string) {
this.matchType = matchType;
this.host = host;
}
}
class RuleConfig<PluginConfig> {
category: Category;
routes!: Map<string, boolean>;
services!: Map<string, boolean>;
routePrefixs!: Map<string, boolean>;
hosts!: Array<HostMatcher>;
config: PluginConfig | null;
constructor() {
this.category = Category.Route;
this.config = null;
}
}
export class ParseResult<PluginConfig> {
pluginConfig: PluginConfig | null;
success: boolean;
constructor(pluginConfig: PluginConfig | null, success: boolean) {
this.pluginConfig = pluginConfig;
this.success = success;
}
}
export class RuleMatcher<PluginConfig> {
ruleConfig: Array<RuleConfig<PluginConfig>>;
globalConfig: PluginConfig | null;
hasGlobalConfig: boolean;
constructor() {
this.ruleConfig = new Array<RuleConfig<PluginConfig>>();
this.globalConfig = null;
this.hasGlobalConfig = false;
}
getMatchConfig(): ParseResult<PluginConfig> {
const host = getRequestHost();
if (host == "") {
return new ParseResult<PluginConfig>(null, false);
}
let result = get_property("route_name");
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
return new ParseResult<PluginConfig>(null, false);
}
const routeName = String.UTF8.decode(result.returnValue);
result = get_property("cluster_name");
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
return new ParseResult<PluginConfig>(null, false);
}
const serviceName = String.UTF8.decode(result.returnValue);
for (let i = 0; i < this.ruleConfig.length; i++) {
const rule = this.ruleConfig[i];
// category == Host
if (rule.category == Category.Host) {
if (this.hostMatch(rule, host)) {
log(LogLevelValues.debug, "getMatchConfig: match host " + host);
return new ParseResult<PluginConfig>(rule.config, true);
}
}
// category == Route
if (rule.category == Category.Route) {
if (rule.routes.has(routeName)) {
log(LogLevelValues.debug, "getMatchConfig: match route " + routeName);
return new ParseResult<PluginConfig>(rule.config, true);
}
}
// category == RoutePrefix
if (rule.category == Category.RoutePrefix) {
for (let i = 0; i < rule.routePrefixs.keys().length; i++) {
const routePrefix = rule.routePrefixs.keys()[i];
if (routeName.startsWith(routePrefix)) {
return new ParseResult<PluginConfig>(rule.config, true);
}
}
}
// category == Cluster
if (this.serviceMatch(rule, serviceName)) {
return new ParseResult<PluginConfig>(rule.config, true);
}
}
if (this.hasGlobalConfig) {
return new ParseResult<PluginConfig>(this.globalConfig, true);
}
return new ParseResult<PluginConfig>(null, false);
}
parseRuleConfig(
config: JSON.Obj,
parsePluginConfig: (json: JSON.Obj) => ParseResult<PluginConfig>
): boolean {
const obj = config;
let keyCount = obj.keys.length;
if (keyCount == 0) {
this.hasGlobalConfig = true;
const parseResult = parsePluginConfig(config);
if (parseResult.success) {
this.globalConfig = parseResult.pluginConfig;
return true;
} else {
return false;
}
}
let rules: JSON.Arr | null = null;
if (obj.has(RULES_KEY)) {
rules = obj.getArr(RULES_KEY);
keyCount--;
}
if (keyCount > 0) {
const parseResult = parsePluginConfig(config);
if (parseResult.success) {
this.globalConfig = parseResult.pluginConfig;
this.hasGlobalConfig = true;
}
}
if (!rules) {
if (this.hasGlobalConfig) {
return true;
}
log(LogLevelValues.error, "parse config failed, no valid rules; global config parse error");
return false;
}
const rulesArray = rules.valueOf();
for (let i = 0; i < rulesArray.length; i++) {
if (!rulesArray[i].isObj) {
log(LogLevelValues.error, "parse rule failed, rules must be an array of objects");
continue;
}
const ruleJson = changetype<JSON.Obj>(rulesArray[i]);
const rule = new RuleConfig<PluginConfig>();
const parseResult = parsePluginConfig(ruleJson);
if (parseResult.success) {
rule.config = parseResult.pluginConfig;
} else {
return false;
}
rule.routes = this.parseRouteMatchConfig(ruleJson);
rule.hosts = this.parseHostMatchConfig(ruleJson);
rule.services = this.parseServiceMatchConfig(ruleJson);
rule.routePrefixs = this.parseRoutePrefixMatchConfig(ruleJson);
const noRoute = rule.routes.size == 0;
const noHosts = rule.hosts.length == 0;
const noServices = rule.services.size == 0;
const noRoutePrefixs = rule.routePrefixs.size == 0;
if ((boolToInt(noRoute) + boolToInt(noHosts) + boolToInt(noServices) + boolToInt(noRoutePrefixs)) != 3) {
log(LogLevelValues.error, "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.");
return false;
}
if (!noRoute) {
rule.category = Category.Route;
} else if (!noHosts) {
rule.category = Category.Host;
} else if (!noServices) {
rule.category = Category.Service;
} else {
rule.category = Category.RoutePrefix;
}
this.ruleConfig.push(rule);
}
return true;
}
parseRouteMatchConfig(config: JSON.Obj): Map<string, boolean> {
const keys = config.getArr(MATCH_ROUTE_KEY);
const routes = new Map<string, boolean>();
if (keys) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const key = array[i].toString();
if (key != "") {
routes.set(key, true);
}
}
}
return routes;
}
parseRoutePrefixMatchConfig(config: JSON.Obj): Map<string, boolean> {
const keys = config.getArr(MATCH_ROUTE_PREFIX_KEY);
const routePrefixs = new Map<string, boolean>();
if (keys) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const key = array[i].toString();
if (key != "") {
routePrefixs.set(key, true);
}
}
}
return routePrefixs;
}
parseServiceMatchConfig(config: JSON.Obj): Map<string, boolean> {
const keys = config.getArr(MATCH_SERVICE_KEY);
const clusters = new Map<string, boolean>();
if (keys) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const key = array[i].toString();
if (key != "") {
clusters.set(key, true);
}
}
}
return clusters;
}
parseHostMatchConfig(config: JSON.Obj): Array<HostMatcher> {
const hostMatchers = new Array<HostMatcher>();
const keys = config.getArr(MATCH_DOMAIN_KEY);
if (keys !== null) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const item = array[i].toString(); // Assuming the array has string elements
let hostMatcher: HostMatcher;
if (item.startsWith("*")) {
hostMatcher = new HostMatcher(MatchType.Suffix, item.substr(1));
} else if (item.endsWith("*")) {
hostMatcher = new HostMatcher(
MatchType.Prefix,
item.substr(0, item.length - 1)
);
} else {
hostMatcher = new HostMatcher(MatchType.Exact, item);
}
hostMatchers.push(hostMatcher);
}
}
return hostMatchers;
}
stripPortFromHost(reqHost: string): string {
// Port removing code is inspired by
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
let portStart: i32 = reqHost.lastIndexOf(":");
if (portStart != -1) {
// According to RFC3986 v6 address is always enclosed in "[]".
// section 3.2.2.
let v6EndIndex: i32 = reqHost.lastIndexOf("]");
if (v6EndIndex == -1 || v6EndIndex < portStart) {
if (portStart + 1 <= reqHost.length) {
return reqHost.substring(0, portStart);
}
}
}
return reqHost;
}
hostMatch(rule: RuleConfig<PluginConfig>, reqHost: string): boolean {
reqHost = this.stripPortFromHost(reqHost);
for (let i = 0; i < rule.hosts.length; i++) {
let hostMatch = rule.hosts[i];
switch (hostMatch.matchType) {
case MatchType.Suffix:
if (reqHost.endsWith(hostMatch.host)) {
return true;
}
break;
case MatchType.Prefix:
if (reqHost.startsWith(hostMatch.host)) {
return true;
}
break;
case MatchType.Exact:
if (reqHost == hostMatch.host) {
return true;
}
break;
default:
return false;
}
}
return false;
}
serviceMatch(rule: RuleConfig<PluginConfig>, serviceName: string): boolean {
const parts = serviceName.split('|');
if (parts.length != 4) {
return false;
}
const port = parts[1];
const fqdn = parts[3];
for (let i = 0; i < rule.services.keys().length; i++) {
let configServiceName = rule.services.keys()[i];
let colonIndex = configServiceName.lastIndexOf(':');
if (colonIndex != -1) {
let configFQDN = configServiceName.slice(0, colonIndex);
let configPort = configServiceName.slice(colonIndex + 1);
if (fqdn == configFQDN && port == configPort) return true;
} else if (fqdn == configServiceName) {
return true;
}
}
return false;
}
}
function boolToInt(value: boolean): i32 {
return value ? 1 : 0;
}

View File

@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

View File

@@ -0,0 +1,80 @@
# 功能说明
`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| -------- | -------- | -------- | -------- | -------- |
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
| headers | array of string | 选填 | - | 自定义 HTTP 应答头key 和 value 用`=`分隔 |
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
# 配置示例
## Mock 应答场景
```yaml
status_code: 200
headers:
- Content-Type=application/json
- Hello=World
body: "{\"hello\":\"world\"}"
```
根据该配置,请求将返回自定义应答如下:
```text
HTTP/1.1 200 OK
Content-Type: application/json
Hello: World
Content-Length: 17
{"hello":"world"}
```
## 触发限流时自定义响应
```yaml
enable_on_status:
- 429
status_code: 302
headers:
- Location=https://example.com
```
触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下:
```text
HTTP/1.1 302 Found
Location: https://example.com
```
从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。
如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。
## 对特定路由或域名开启
```yaml
# 使用 matchRules 字段进行细粒度规则配置
matchRules:
# 规则一:按 Ingress 名称匹配生效
- ingress:
- default/foo
- default/bar
body: "{\"hello\":\"world\"}"
# 规则二:按域名匹配生效
- domain:
- "*.example.com"
- test.com
enable_on_status:
- 429
status_code: 200
headers:
- Content-Type=application/json
body: "{\"errmsg\": \"rate limited\"}"
```
此例 `ingress` 中指定的 `default/foo``default/bar` 对应 default 命名空间下名为 foo 和 bar 的 Ingress当匹配到这两个 Ingress 时,将使用此段配置;
此例 `domain` 中指定的 `*.example.com``test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
配置的匹配生效顺序,将按照 `matchRules` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。

View File

@@ -0,0 +1,24 @@
{
"targets": {
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false,
"debug": true
}
},
"options": {
"bindings": "esm",
"use": "abort=abort_proc_exit"
}
}

View File

@@ -0,0 +1,96 @@
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseConfigBy, ParseResult, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
import { FilterHeadersStatusValues, Headers, send_http_response, stream_context, HeaderPair } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
import { JSON } from "assemblyscript-json/assembly";
class CustomResponseConfig {
statusCode: u32;
headers: Headers;
body: ArrayBuffer;
enableOnStatus: Array<u32>;
contentType: string;
constructor() {
this.statusCode = 200;
this.headers = [];
this.body = new ArrayBuffer(0);
this.enableOnStatus = [];
this.contentType = "text/plain; charset=utf-8";
}
}
SetCtx<CustomResponseConfig>(
"custom-response",
[ParseConfigBy<CustomResponseConfig>(parseConfig),
ProcessRequestHeadersBy<CustomResponseConfig>(onHttpRequestHeaders),
ProcessResponseHeadersBy<CustomResponseConfig>(onHttpResponseHeaders),])
function parseConfig(json: JSON.Obj): ParseResult<CustomResponseConfig> {
let headersArray = json.getArr("headers");
let config = new CustomResponseConfig();
if (headersArray != null) {
for (let i = 0; i < headersArray.valueOf().length; i++) {
let header = headersArray._arr[i];
let jsonString = (<JSON.Str>header).toString()
let kv = jsonString.split("=")
if (kv.length == 2) {
let key = kv[0].trim();
let value = kv[1].trim();
if (key.toLowerCase() == "content-type") {
config.contentType = value;
} else if (key.toLowerCase() == "content-length") {
continue;
} else {
config.headers.push(new HeaderPair(String.UTF8.encode(key), String.UTF8.encode(value)));
}
} else {
Logger.Error("parse header failed");
return new ParseResult<CustomResponseConfig>(null, false);
}
}
}
let body = json.getString("body");
if (body != null) {
config.body = String.UTF8.encode(body.valueOf());
}
config.headers.push(new HeaderPair(String.UTF8.encode("content-type"), String.UTF8.encode(config.contentType)));
let statusCode = json.getInteger("statusCode");
if (statusCode != null) {
config.statusCode = statusCode.valueOf() as u32;
}
let enableOnStatus = json.getArr("enableOnStatus");
if (enableOnStatus != null) {
for (let i = 0; i < enableOnStatus.valueOf().length; i++) {
let status = enableOnStatus._arr[i];
if (status.isInteger) {
config.enableOnStatus.push((<JSON.Integer>status).valueOf() as u32);
}
}
}
return new ParseResult<CustomResponseConfig>(config, true);
}
function onHttpRequestHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
if (config.enableOnStatus.length != 0) {
return FilterHeadersStatusValues.Continue;
}
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
return FilterHeadersStatusValues.StopIteration;
}
function onHttpResponseHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
let statusCodeStr = stream_context.headers.response.get(":status")
if (statusCodeStr == "") {
Logger.Error("get http response status code failed");
return FilterHeadersStatusValues.Continue;
}
let statusCode = parseInt(statusCodeStr);
for (let i = 0; i < config.enableOnStatus.length; i++) {
if (statusCode == config.enableOnStatus[i]) {
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
}
}
return FilterHeadersStatusValues.Continue;
}

View File

@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

View File

@@ -0,0 +1,68 @@
{
"name": "custom-response",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "custom-response",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@higress/wasm-assemblyscript": "^0.0.4",
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0"
}
},
"node_modules/@higress/wasm-assemblyscript": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.4.tgz",
"integrity": "sha512-F9m3fHBeM0OFWWHekTcmj3dVh7I4pbzf0oIioVdArD2oSUgpCZ8ur8E/9r7JR3WVwn2/l0A3LRSBOJTzQnHtMw==",
"dev": true
},
"node_modules/assemblyscript": {
"version": "0.27.29",
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
"dev": true,
"dependencies": {
"binaryen": "116.0.0-nightly.20240114",
"long": "^5.2.1"
},
"bin": {
"asc": "bin/asc.js",
"asinit": "bin/asinit.js"
},
"engines": {
"node": ">=16",
"npm": ">=7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/assemblyscript"
}
},
"node_modules/assemblyscript-json": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
"dev": true
},
"node_modules/binaryen": {
"version": "116.0.0-nightly.20240114",
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
"dev": true,
"bin": {
"wasm-opt": "bin/wasm-opt",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
"dev": true
}
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "custom-response",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "node tests",
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0",
"@higress/wasm-assemblyscript": "^0.0.4"
},
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
}
}

View File

@@ -0,0 +1,24 @@
{
"targets": {
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false,
"debug": true
}
},
"options": {
"bindings": "esm",
"use": "abort=abort_proc_exit"
}
}

View File

@@ -0,0 +1,42 @@
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseResult, ParseConfigBy, RegisteTickFunc, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
import { FilterHeadersStatusValues, send_http_response, stream_context } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
import { JSON } from "assemblyscript-json/assembly";
class HelloWorldConfig {
}
SetCtx<HelloWorldConfig>("hello-world",
[ParseConfigBy<HelloWorldConfig>(parseConfig),
ProcessRequestHeadersBy<HelloWorldConfig>(onHttpRequestHeaders),
ProcessResponseHeadersBy<HelloWorldConfig>(onHttpResponseHeaders)
])
function parseConfig(json: JSON.Obj): ParseResult<HelloWorldConfig> {
RegisteTickFunc(2000, () => {
Logger.Debug("tick 2s");
})
RegisteTickFunc(5000, () => {
Logger.Debug("tick 5s");
})
return new ParseResult<HelloWorldConfig>(new HelloWorldConfig(), true);
}
class TestContext{
value: string
constructor(value: string){
this.value = value
}
}
function onHttpRequestHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
stream_context.headers.request.add("hello", "world");
Logger.Debug("[hello-world] logger test");
context.SetContext("test-set-context", changetype<usize>(new TestContext("value")))
send_http_response(200, "hello-world", String.UTF8.encode("[wasm-assemblyscript]hello world"), []);
return FilterHeadersStatusValues.Continue;
}
function onHttpResponseHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
const str = changetype<TestContext>(context.GetContext("test-set-context")).value;
Logger.Debug("[hello-world] test-set-context: " + str);
return FilterHeadersStatusValues.Continue;
}

View File

@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

View File

@@ -0,0 +1,68 @@
{
"name": "hello-world",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hello-world",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@higress/wasm-assemblyscript": "^0.0.4",
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0"
}
},
"node_modules/@higress/wasm-assemblyscript": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.4.tgz",
"integrity": "sha512-F9m3fHBeM0OFWWHekTcmj3dVh7I4pbzf0oIioVdArD2oSUgpCZ8ur8E/9r7JR3WVwn2/l0A3LRSBOJTzQnHtMw==",
"dev": true
},
"node_modules/assemblyscript": {
"version": "0.27.29",
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
"dev": true,
"dependencies": {
"binaryen": "116.0.0-nightly.20240114",
"long": "^5.2.1"
},
"bin": {
"asc": "bin/asc.js",
"asinit": "bin/asinit.js"
},
"engines": {
"node": ">=16",
"npm": ">=7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/assemblyscript"
}
},
"node_modules/assemblyscript-json": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
"dev": true
},
"node_modules/binaryen": {
"version": "116.0.0-nightly.20240114",
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
"dev": true,
"bin": {
"wasm-opt": "bin/wasm-opt",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
"dev": true
}
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "hello-world",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "node tests",
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0",
"@higress/wasm-assemblyscript": "^0.0.4"
},
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
}
}

View File

@@ -0,0 +1,75 @@
{
"name": "@higress/wasm-assemblyscript",
"version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@higress/wasm-assemblyscript",
"version": "0.0.4",
"license": "Apache-2.0",
"devDependencies": {
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
"as-uuid": "^0.0.4",
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0"
}
},
"node_modules/@higress/proxy-wasm-assemblyscript-sdk": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/@higress/proxy-wasm-assemblyscript-sdk/-/proxy-wasm-assemblyscript-sdk-0.0.2.tgz",
"integrity": "sha512-0J1tFJMTE6o37JpGJBLq0wc5kBC/fpbISrP+KFb4bAEeshu6daXzD2P3bAfJXmW+oZdY0WGptTGXWx8pf9Fk+g==",
"dev": true
},
"node_modules/as-uuid": {
"version": "0.0.4",
"resolved": "https://registry.npmmirror.com/as-uuid/-/as-uuid-0.0.4.tgz",
"integrity": "sha512-ZHNv0ETSzg5ZD0IWWJVyip/73LWtrWeMmvRi+16xbkpU/nZ0O8EegvgS7bgZ5xRqrUbc2NqZqHOWMOtPqbLrhg==",
"dev": true
},
"node_modules/assemblyscript": {
"version": "0.27.29",
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
"dev": true,
"dependencies": {
"binaryen": "116.0.0-nightly.20240114",
"long": "^5.2.1"
},
"bin": {
"asc": "bin/asc.js",
"asinit": "bin/asinit.js"
},
"engines": {
"node": ">=16",
"npm": ">=7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/assemblyscript"
}
},
"node_modules/assemblyscript-json": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
"dev": true
},
"node_modules/binaryen": {
"version": "116.0.0-nightly.20240114",
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
"dev": true,
"bin": {
"wasm-opt": "bin/wasm-opt",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
"dev": true
}
}
}

View File

@@ -0,0 +1,37 @@
{
"name": "@higress/wasm-assemblyscript",
"version": "0.0.4",
"main": "assembly/index.ts",
"scripts": {
"test": "node tests",
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
},
"author": "jingze.dai",
"license": "Apache-2.0",
"description": "",
"devDependencies": {
"assemblyscript": "^0.27.29",
"as-uuid": "^0.0.4",
"assemblyscript-json": "^1.1.0",
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2"
},
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
},
"files": [
"/assembly",
"package-lock.json",
"index.js"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Jing-ze/wasm-assemblyscript.git"
}
}

View File

@@ -5,7 +5,7 @@ GO_VERSION ?= 1.19
TINYGO_VERSION ?= 0.28.1
ORAS_VERSION ?= 1.0.0
HIGRESS_VERSION ?= 1.0.0-rc
USE_HIGRESS_TINYGO ?= true
USE_HIGRESS_TINYGO ?= false
BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-tinygo${TINYGO_VERSION}-oras${ORAS_VERSION}
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)

View File

@@ -0,0 +1,350 @@
---
title: AI Agent
keywords: [ AI网关, AI Agent ]
description: AI Agent插件配置参考
---
## 功能说明
一个可定制化的 API AI Agent支持配置 http method 类型为 GET 与 POST 的 API目前只支持非流式模式。
agent流程图如下
![ai-agent](https://github.com/user-attachments/assets/b0761a0c-1afa-496c-a98e-bb9f38b340f8)
## 配置字段
### 基本配置
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|------------------|-----------|---------|--------|----------------------------|
| `llm` | object | 必填 | - | 配置 AI 服务提供商的信息 |
| `apis` | object | 必填 | - | 配置外部 API 服务提供商的信息 |
| `promptTemplate` | object | 非必填 | - | 配置 Agent ReAct 模板的信息 |
`llm`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|--------------------|-----------|---------|--------|-----------------------------------|
| `apiKey` | string | 必填 | - | 用于在访问大模型服务时进行认证的令牌。|
| `serviceName` | string | 必填 | - | 大模型服务名 |
| `servicePort` | int | 必填 | - | 大模型服务端口 |
| `domain` | string | 必填 | - | 访问大模型服务时域名 |
| `path` | string | 必填 | - | 访问大模型服务时路径 |
| `model` | string | 必填 | - | 访问大模型服务时模型名 |
| `maxIterations` | int | 必填 | 15 | 结束执行循环前的最大步数 |
| `maxExecutionTime` | int | 必填 | 50000 | 每一次请求大模型的超时时间,单位毫秒 |
| `maxTokens` | int | 必填 | 1000 | 每一次请求大模型的输出token限制 |
`apis`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-----------------|-----------|---------|--------|-----------------------------------|
| `apiProvider` | object | 必填 | - | 外部 API 服务信息 |
| `api` | string | 必填 | - | 工具的 OpenAPI 文档 |
`apiProvider`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-----------------|-----------|---------|--------|------------------------------------------|
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
| `serviceName` | string | 必填 | - | 访问外部 API 服务名 |
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
`apiKey`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-------------------|---------|------------|--------|-------------------------------------------------------------------------------|
| `in` | string | 非必填 | header | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,默认是 header。
| `name` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的名称。 |
| `value` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的值。 |
`promptTemplate`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-----------------|-----------|-----------|--------|--------------------------------------------|
| `language` | string | 非必填 | EN | Agent ReAct 模板的语言类型,包括 CH 和 EN 两种|
| `chTemplate` | object | 非必填 | - | Agent ReAct 中文模板 |
| `enTemplate` | object | 非必填 | - | Agent ReAct 英文模板 |
`chTemplate``enTemplate`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-----------------|-----------|-----------|--------|---------------------------------------------|
| `question` | string | 非必填 | - | Agent ReAct 模板的 question 部分 |
| `thought1` | string | 非必填 | - | Agent ReAct 模板的 thought1 部分 |
| `actionInput` | string | 非必填 | - | Agent ReAct 模板的 actionInput 部分 |
| `observation` | string | 非必填 | - | Agent ReAct 模板的 observation 部分 |
| `thought2` | string | 非必填 | - | Agent ReAct 模板的 thought2 部分 |
| `finalAnswer` | string | 非必填 | - | Agent ReAct 模板的 finalAnswer 部分 |
| `begin` | string | 非必填 | - | Agent ReAct 模板的 begin 部分 |
## 用法示例
**配置信息**
```yaml
llm:
apiKey: xxxxxxxxxxxxxxxxxx
domain: dashscope.aliyuncs.com
serviceName: dashscope.dns
servicePort: 443
path: /compatible-mode/v1/chat/completions
model: qwen-max-0403
maxIterations: 2
promptTemplate:
language: CH
apis:
- apiProvider:
domain: restapi.amap.com
serviceName: geo.dns
servicePort: 80
apiKey:
in: query
name: key
value: xxxxxxxxxxxxxxx
api: |
openapi: 3.1.0
info:
title: 高德地图
description: 获取 POI 的相关信息
version: v1.0.0
servers:
- url: https://restapi.amap.com
paths:
/v5/place/text:
get:
description: 根据POI名称获得POI的经纬度坐标
operationId: get_location_coordinate
parameters:
- name: keywords
in: query
description: POI名称必须是中文
required: true
schema:
type: string
- name: region
in: query
description: POI所在的区域名必须是中文
required: true
schema:
type: string
deprecated: false
/v5/place/around:
get:
description: 搜索给定坐标附近的POI
operationId: search_nearby_pois
parameters:
- name: keywords
in: query
description: 目标POI的关键字
required: true
schema:
type: string
- name: location
in: query
description: 中心点的经度和纬度,用逗号隔开
required: true
schema:
type: string
deprecated: false
components:
schemas: {}
- apiProvider:
domain: api.seniverse.com
serviceName: seniverse.dns
servicePort: 80
apiKey:
in: query
name: key
value: xxxxxxxxxxxxxxx
api: |
openapi: 3.1.0
info:
title: 心知天气
description: 获取 天气预办相关信息
version: v1.0.0
servers:
- url: https://api.seniverse.com
paths:
/v3/weather/now.json:
get:
description: 获取指定城市的天气实况
operationId: get_weather_now
parameters:
- name: location
in: query
description: 所查询的城市
required: true
schema:
type: string
- name: language
in: query
description: 返回天气查询结果所使用的语言
required: true
schema:
type: string
default: zh-Hans
enum:
- zh-Hans
- en
- ja
- name: unit
in: query
description: 表示温度的的单位,有摄氏度和华氏度两种
required: true
schema:
type: string
default: c
enum:
- c
- f
deprecated: false
components:
schemas: {}
- apiProvider:
apiKey:
in: "header"
name: "DeepL-Auth-Key"
value: "73xxxxxxxxxxxxxxx:fx"
domain: "api-free.deepl.com"
serviceName: "deepl.dns"
servicePort: 443
api: |
openapi: 3.1.0
info:
title: DeepL API Documentation
description: The DeepL API provides programmatic access to DeepLs machine translation technology.
version: v1.0.0
servers:
- url: https://api-free.deepl.com/v2
paths:
/translate:
post:
summary: Request Translation
operationId: translateText
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- text
- target_lang
properties:
text:
description: |
Text to be translated. Only UTF-8-encoded plain text is supported. The parameter may be specified
up to 50 times in a single request. Translations are returned in the same order as they are requested.
type: array
maxItems: 50
items:
type: string
example: Hello, World!
target_lang:
description: The language into which the text should be translated.
type: string
enum:
- BG
- CS
- DA
- DE
- EL
- EN-GB
- EN-US
- ES
- ET
- FI
- FR
- HU
- ID
- IT
- JA
- KO
- LT
- LV
- NB
- NL
- PL
- PT-BR
- PT-PT
- RO
- RU
- SK
- SL
- SV
- TR
- UK
- ZH
- ZH-HANS
example: DE
components:
schemas: {}
```
本示例配置了三个服务演示了get与post两种类型的工具。其中get类型的工具包括高德地图与心知天气post类型的工具是deepl翻译。三个服务都需要现在Higress的服务中以DNS域名的方式配置好并确保健康。
高德地图提供了两个工具分别是获取指定地点的坐标以及搜索坐标附近的感兴趣的地点。文档https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch
心知天气提供了一个工具用于获取指定城市的实时天气情况支持中文英文日语返回以及摄氏度和华氏度的表示。文档https://seniverse.yuque.com/hyper_data/api_v3/nyiu3t
deepl提供了一个工具用于翻译给定的句子支持多语言。。文档https://developers.deepl.com/docs/v/zh/api-reference/translate?fallback=true
以下为测试用例为了效果的稳定性建议保持大模型版本的稳定本例子中使用的qwen-max-0403
**请求示例**
```shell
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"我想在济南市鑫盛大厦附近喝咖啡,给我推荐几个"}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**响应示例**
```json
{"id":"139487e7-96a0-9b13-91b4-290fb79ac992","choices":[{"index":0,"message":{"role":"assistant","content":" 在济南市鑫盛大厦附近,您可以选择以下咖啡店:\n1. luckin coffee 瑞幸咖啡(鑫盛大厦店)位于新泺大街1299号鑫盛大厦2号楼大堂\n2. 三庆齐盛广场挪瓦咖啡(三庆·齐盛广场店)位于新泺大街与颖秀路交叉口西南60米\n3. luckin coffee 瑞幸咖啡(三庆·齐盛广场店)位于颖秀路1267号\n4. 库迪咖啡(齐鲁软件园店)位于新泺大街三庆齐盛广场4号楼底商\n5. 库迪咖啡(美莲广场店)位于高新区新泺大街1166号美莲广场L117号以及其他一些选项。希望这些建议对您有所帮助"},"finish_reason":"stop"}],"created":1723172296,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":886,"completion_tokens":50,"total_tokens":936}}
```
**请求示例**
```shell
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**响应示例**
```json
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" 济南市现在的天气状况为阴天温度为31℃。此信息最后更新于2024年8月9日15时12分北京时间。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
```
**请求示例**
```shell
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?用华氏度表示,用日语回答"}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**响应示例**
```json
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" 济南市の現在の天気は雨曇りで、気温は88°Fです。この情報は2024年8月9日15時12分東京時間に更新されました。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
```
**请求示例**
```shell
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"帮我用德语翻译以下句子:九头蛇万岁!"}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**响应示例**
```json
{"id":"65dcf12c-61ff-9e68-bffa-44fc9e6070d5","choices":[{"index":0,"message":{"role":"assistant","content":" “九头蛇万岁!”的德语翻译为“Hoch lebe Hydra!”。"},"finish_reason":"stop"}],"created":1724043865,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":908,"completion_tokens":52,"total_tokens":960}}
```

View File

@@ -0,0 +1,424 @@
package main
import (
"encoding/json"
"errors"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tidwall/gjson"
"gopkg.in/yaml.v2"
)
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type Request struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
FrequencyPenalty float64 `json:"frequency_penalty"`
PresencePenalty float64 `json:"presence_penalty"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature"`
Topp int32 `json:"top_p"`
}
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
}
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type Response struct {
ID string `json:"id"`
Choices []Choice `json:"choices"`
Created int64 `json:"created"`
Model string `json:"model"`
Object string `json:"object"`
Usage Usage `json:"usage"`
}
// 用于存放拆解出来的工具相关信息
type Tool_Param struct {
ToolName string `yaml:"toolName"`
Path string `yaml:"path"`
Method string `yaml:"method"`
ParamName []string `yaml:"paramName"`
Parameter string `yaml:"parameter"`
Description string `yaml:"description"`
}
// 用于存放拆解出来的api相关信息
type APIParam struct {
APIKey APIKey `yaml:"apiKey"`
URL string `yaml:"url"`
Tool_Param []Tool_Param `yaml:"tool_Param"`
}
type Info struct {
Title string `yaml:"title"`
Description string `yaml:"description"`
Version string `yaml:"version"`
}
type Server struct {
URL string `yaml:"url"`
}
// 给OpenAPI的get方法用的
type Parameter struct {
Name string `yaml:"name"`
In string `yaml:"in"`
Description string `yaml:"description"`
Required bool `yaml:"required"`
Schema struct {
Type string `yaml:"type"`
Default string `yaml:"default"`
Enum []string `yaml:"enum"`
} `yaml:"schema"`
}
type Items struct {
Type string `yaml:"type"`
Example string `yaml:"example"`
}
type Property struct {
Description string `yaml:"description"`
Type string `yaml:"type"`
Enum []string `yaml:"enum,omitempty"`
Items *Items `yaml:"items,omitempty"`
MaxItems int `yaml:"maxItems,omitempty"`
Example string `yaml:"example,omitempty"`
}
type Schema struct {
Type string `yaml:"type"`
Required []string `yaml:"required"`
Properties map[string]Property `yaml:"properties"`
}
type MediaType struct {
Schema Schema `yaml:"schema"`
}
// 给OpenAPI的post方法用的
type RequestBody struct {
Required bool `yaml:"required"`
Content map[string]MediaType `yaml:"content"`
}
type PathItem struct {
Description string `yaml:"description"`
Summary string `yaml:"summary"`
OperationID string `yaml:"operationId"`
RequestBody RequestBody `yaml:"requestBody"`
Parameters []Parameter `yaml:"parameters"`
Deprecated bool `yaml:"deprecated"`
}
type Paths map[string]map[string]PathItem
type Components struct {
Schemas map[string]interface{} `yaml:"schemas"`
}
type API struct {
OpenAPI string `yaml:"openapi"`
Info Info `yaml:"info"`
Servers []Server `yaml:"servers"`
Paths Paths `yaml:"paths"`
Components Components `yaml:"components"`
}
type APIKey struct {
In string `yaml:"in" json:"in"`
Name string `yaml:"name" json:"name"`
Value string `yaml:"value" json:"value"`
}
type APIProvider struct {
// @Title zh-CN 服务名称
// @Description zh-CN 带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
// @Title zh-CN 服务端口
// @Description zh-CN 服务端口
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
// @Title zh-CN 服务域名
// @Description zh-CN 服务域名,例如 restapi.amap.com
Domin string `required:"true" yaml:"domain" json:"domain"`
// @Title zh-CN 通义千问大模型服务的key
// @Description zh-CN 通义千问大模型服务的key
APIKey APIKey `required:"true" yaml:"apiKey" json:"apiKey"`
}
type APIs struct {
APIProvider APIProvider `required:"true" yaml:"apiProvider" json:"apiProvider"`
API string `required:"true" yaml:"api" json:"api"`
}
type Template struct {
Question string `yaml:"question" json:"question"`
Thought1 string `yaml:"thought1" json:"thought1"`
ActionInput string `yaml:"actionInput" json:"actionInput"`
Observation string `yaml:"observation" json:"observation"`
Thought2 string `yaml:"thought2" json:"thought2"`
FinalAnswer string `yaml:"finalAnswer" json:"finalAnswer"`
Begin string `yaml:"begin" json:"begin"`
}
type PromptTemplate struct {
Language string `required:"true" yaml:"language" json:"language"`
CHTemplate Template `yaml:"chTemplate" json:"chTemplate"`
ENTemplate Template `yaml:"enTemplate" json:"enTemplate"`
}
type LLMInfo struct {
// @Title zh-CN 大模型服务名称
// @Description zh-CN 带服务类型的完整 FQDN 名称
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
// @Title zh-CN 大模型服务端口
// @Description zh-CN 服务端口
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
// @Title zh-CN 大模型服务域名
// @Description zh-CN 大模型服务域名,例如 dashscope.aliyuncs.com
Domin string `required:"true" yaml:"domin" json:"domin"`
// @Title zh-CN 大模型服务的key
// @Description zh-CN 大模型服务的key
APIKey string `required:"true" yaml:"apiKey" json:"apiKey"`
// @Title zh-CN 大模型服务的请求路径
// @Description zh-CN 大模型服务的请求路径,如"/compatible-mode/v1/chat/completions"
Path string `required:"true" yaml:"path" json:"path"`
// @Title zh-CN 大模型服务的模型名称
// @Description zh-CN 大模型服务的模型名称,如"qwen-max-0403"
Model string `required:"true" yaml:"model" json:"model"`
// @Title zh-CN 结束执行循环前的最大步数
// @Description zh-CN 结束执行循环前的最大步数比如2设置为0可能会无限循环直到超时退出默认15
MaxIterations int64 `yaml:"maxIterations" json:"maxIterations"`
// @Title zh-CN 每一次请求大模型的超时时间
// @Description zh-CN 每一次请求大模型的超时时间单位毫秒默认50000
MaxExecutionTime int64 `yaml:"maxExecutionTime" json:"maxExecutionTime"`
// @Title zh-CN
// @Description zh-CN 每一次请求大模型的输出token限制默认1000
MaxTokens int64 `yaml:"maxToken" json:"maxTokens"`
}
type PluginConfig struct {
// @Title zh-CN 返回 HTTP 响应的模版
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
ReturnResponseTemplate string `required:"true" yaml:"returnResponseTemplate" json:"returnResponseTemplate"`
// @Title zh-CN 工具服务商以及工具信息
// @Description zh-CN 用于存储工具服务商以及工具信息
APIs []APIs `required:"true" yaml:"apis" json:"apis"`
APIClient []wrapper.HttpClient `yaml:"-" json:"-"`
// @Title zh-CN llm信息
// @Description zh-CN 用于存储llm使用信息
LLMInfo LLMInfo `required:"true" yaml:"llm" json:"llm"`
LLMClient wrapper.HttpClient `yaml:"-" json:"-"`
APIParam []APIParam `yaml:"-" json:"-"`
PromptTemplate PromptTemplate `yaml:"promptTemplate" json:"promptTemplate"`
}
func initResponsePromptTpl(gjson gjson.Result, c *PluginConfig) {
//设置回复模板
c.ReturnResponseTemplate = gjson.Get("returnResponseTemplate").String()
if c.ReturnResponseTemplate == "" {
c.ReturnResponseTemplate = `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
}
}
func initAPIs(gjson gjson.Result, c *PluginConfig) error {
//从插件配置中获取apis信息
apis := gjson.Get("apis")
if !apis.Exists() {
return errors.New("apis is required")
}
if len(apis.Array()) == 0 {
return errors.New("apis cannot be empty")
}
for _, item := range apis.Array() {
serviceName := item.Get("apiProvider.serviceName")
if !serviceName.Exists() || serviceName.String() == "" {
return errors.New("apiProvider serviceName is required")
}
servicePort := item.Get("apiProvider.servicePort")
if !servicePort.Exists() || servicePort.Int() == 0 {
return errors.New("apiProvider servicePort is required")
}
domain := item.Get("apiProvider.domain")
if !domain.Exists() || domain.String() == "" {
return errors.New("apiProvider domain is required")
}
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
if apiKeyIn != "query" {
apiKeyIn = "header"
}
apiKeyName := item.Get("apiProvider.apiKey.name")
apiKeyValue := item.Get("apiProvider.apiKey.value")
//根据多个toolsClientInfo的信息分别初始化toolsClient
apiClient := wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName.String(),
Port: servicePort.Int(),
Host: domain.String(),
})
c.APIClient = append(c.APIClient, apiClient)
api := item.Get("api")
if !api.Exists() || api.String() == "" {
return errors.New("api is required")
}
var apiStruct API
err := yaml.Unmarshal([]byte(api.String()), &apiStruct)
if err != nil {
return err
}
var allTool_param []Tool_Param
//拆除服务下面的每个api的path
for path, pathmap := range apiStruct.Paths {
//拆解出每个api对应的参数
for method, submap := range pathmap {
//把参数列表存起来
var param Tool_Param
param.Path = path
param.ToolName = submap.OperationID
if method == "get" {
param.Method = "GET"
paramName := make([]string, 0)
for _, parammeter := range submap.Parameters {
paramName = append(paramName, parammeter.Name)
}
param.ParamName = paramName
out, _ := json.Marshal(submap.Parameters)
param.Parameter = string(out)
param.Description = submap.Description
} else if method == "post" {
param.Method = "POST"
schema := submap.RequestBody.Content["application/json"].Schema
param.ParamName = schema.Required
param.Description = submap.Summary
out, _ := json.Marshal(schema.Properties)
param.Parameter = string(out)
}
allTool_param = append(allTool_param, param)
}
}
apiParam := APIParam{
APIKey: APIKey{In: apiKeyIn, Name: apiKeyName.String(), Value: apiKeyValue.String()},
URL: apiStruct.Servers[0].URL,
Tool_Param: allTool_param,
}
c.APIParam = append(c.APIParam, apiParam)
}
return nil
}
func initReActPromptTpl(gjson gjson.Result, c *PluginConfig) {
c.PromptTemplate.Language = gjson.Get("promptTemplate.language").String()
if c.PromptTemplate.Language != "EN" && c.PromptTemplate.Language != "CH" {
c.PromptTemplate.Language = "EN"
}
if c.PromptTemplate.Language == "EN" {
c.PromptTemplate.ENTemplate.Question = gjson.Get("promptTemplate.enTemplate.question").String()
if c.PromptTemplate.ENTemplate.Question == "" {
c.PromptTemplate.ENTemplate.Question = "the input question you must answer"
}
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought1").String()
if c.PromptTemplate.ENTemplate.Thought1 == "" {
c.PromptTemplate.ENTemplate.Thought1 = "you should always think about what to do"
}
c.PromptTemplate.ENTemplate.ActionInput = gjson.Get("promptTemplate.enTemplate.actionInput").String()
if c.PromptTemplate.ENTemplate.ActionInput == "" {
c.PromptTemplate.ENTemplate.ActionInput = "the input to the action"
}
c.PromptTemplate.ENTemplate.Observation = gjson.Get("promptTemplate.enTemplate.observation").String()
if c.PromptTemplate.ENTemplate.Observation == "" {
c.PromptTemplate.ENTemplate.Observation = "the result of the action"
}
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought2").String()
if c.PromptTemplate.ENTemplate.Thought1 == "" {
c.PromptTemplate.ENTemplate.Thought1 = "I now know the final answer"
}
c.PromptTemplate.ENTemplate.FinalAnswer = gjson.Get("promptTemplate.enTemplate.finalAnswer").String()
if c.PromptTemplate.ENTemplate.FinalAnswer == "" {
c.PromptTemplate.ENTemplate.FinalAnswer = "the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content."
}
c.PromptTemplate.ENTemplate.Begin = gjson.Get("promptTemplate.enTemplate.begin").String()
if c.PromptTemplate.ENTemplate.Begin == "" {
c.PromptTemplate.ENTemplate.Begin = "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Arg\"s"
}
} else if c.PromptTemplate.Language == "CH" {
c.PromptTemplate.CHTemplate.Question = gjson.Get("promptTemplate.chTemplate.question").String()
if c.PromptTemplate.CHTemplate.Question == "" {
c.PromptTemplate.CHTemplate.Question = "你需要回答的输入问题"
}
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought1").String()
if c.PromptTemplate.CHTemplate.Thought1 == "" {
c.PromptTemplate.CHTemplate.Thought1 = "你应该总是思考该做什么"
}
c.PromptTemplate.CHTemplate.ActionInput = gjson.Get("promptTemplate.chTemplate.actionInput").String()
if c.PromptTemplate.CHTemplate.ActionInput == "" {
c.PromptTemplate.CHTemplate.ActionInput = "行动的输入必须出现在Action后"
}
c.PromptTemplate.CHTemplate.Observation = gjson.Get("promptTemplate.chTemplate.observation").String()
if c.PromptTemplate.CHTemplate.Observation == "" {
c.PromptTemplate.CHTemplate.Observation = "行动的结果"
}
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought2").String()
if c.PromptTemplate.CHTemplate.Thought1 == "" {
c.PromptTemplate.CHTemplate.Thought1 = "我现在知道最终答案"
}
c.PromptTemplate.CHTemplate.FinalAnswer = gjson.Get("promptTemplate.chTemplate.finalAnswer").String()
if c.PromptTemplate.CHTemplate.FinalAnswer == "" {
c.PromptTemplate.CHTemplate.FinalAnswer = "对原始输入问题的最终答案"
}
c.PromptTemplate.CHTemplate.Begin = gjson.Get("promptTemplate.chTemplate.begin").String()
if c.PromptTemplate.CHTemplate.Begin == "" {
c.PromptTemplate.CHTemplate.Begin = "再次重申,不要修改以上模板的字段名称,开始吧!"
}
}
}
func initLLMClient(gjson gjson.Result, c *PluginConfig) {
c.LLMInfo.APIKey = gjson.Get("llm.apiKey").String()
c.LLMInfo.ServiceName = gjson.Get("llm.serviceName").String()
c.LLMInfo.ServicePort = gjson.Get("llm.servicePort").Int()
c.LLMInfo.Domin = gjson.Get("llm.domain").String()
c.LLMInfo.Path = gjson.Get("llm.path").String()
c.LLMInfo.Model = gjson.Get("llm.model").String()
c.LLMInfo.MaxIterations = gjson.Get("llm.maxIterations").Int()
if c.LLMInfo.MaxIterations == 0 {
c.LLMInfo.MaxIterations = 15
}
c.LLMInfo.MaxExecutionTime = gjson.Get("llm.maxExecutionTime").Int()
if c.LLMInfo.MaxExecutionTime == 0 {
c.LLMInfo.MaxExecutionTime = 50000
}
c.LLMInfo.MaxTokens = gjson.Get("llm.maxTokens").Int()
if c.LLMInfo.MaxTokens == 0 {
c.LLMInfo.MaxTokens = 1000
}
c.LLMClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: c.LLMInfo.ServiceName,
Port: c.LLMInfo.ServicePort,
Host: c.LLMInfo.Domin,
})
}

View File

@@ -0,0 +1,46 @@
package dashscope
var MessageStore ChatMessages
func init() {
MessageStore = make(ChatMessages, 0)
MessageStore.Clear() //清理和初始化
}
type ChatMessages []Message
// 枚举出角色
const (
RoleUser = "user"
RoleAssistant = "assistant"
RoleSystem = "system"
)
func (cm *ChatMessages) Clear() {
*cm = make([]Message, 0) //重新初始化
}
// 添加角色和对应的prompt
func (cm *ChatMessages) AddFor(msg string, role string) {
*cm = append(*cm, Message{
Role: role,
Content: msg,
})
}
// 添加Assistant角色的prompt
func (cm *ChatMessages) AddForAssistant(msg string) {
cm.AddFor(msg, RoleAssistant)
}
// 添加System角色的prompt
func (cm *ChatMessages) AddForSystem(msg string) {
cm.AddFor(msg, RoleSystem)
}
// 添加User角色的prompt
func (cm *ChatMessages) AddForUser(msg string) {
cm.AddFor(msg, RoleUser)
}

View File

@@ -0,0 +1,70 @@
package dashscope
// DashScope embedding service: Request
type Request struct {
Model string `json:"model"`
Input Input `json:"input"`
Parameter Parameter `json:"parameters"`
}
type Input struct {
Texts []string `json:"texts"`
}
type Parameter struct {
TextType string `json:"text_type"`
}
// DashScope embedding service: Response
type Response struct {
Output Output `json:"output"`
Usage Usage `json:"usage"`
RequestID string `json:"request_id"`
}
type Output struct {
Embeddings []Embedding `json:"embeddings"`
}
type Embedding struct {
Embedding []float32 `json:"embedding"`
TextIndex int32 `json:"text_index"`
}
type Usage struct {
TotalTokens int32 `json:"total_tokens"`
}
// completion
type Completion struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
MaxTokens int64 `json:"max_tokens"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type CompletionResponse struct {
Choices []Choice `json:"choices"`
Object string `json:"object"`
Usage CompletionUsage `json:"usage"`
Created string `json:"created"`
SystemFingerprint string `json:"system_fingerprint"`
Model string `json:"model"`
ID string `json:"id"`
}
type Choice struct {
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
Index int `json:"index"`
}
type CompletionUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}

View File

@@ -0,0 +1,19 @@
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent
go 1.19
require (
github.com/alibaba/higress/plugins/wasm-go v1.4.2
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
github.com/tidwall/gjson v1.17.3
gopkg.in/yaml.v2 v2.4.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.14.0 // 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
)

View File

@@ -0,0 +1,26 @@
github.com/alibaba/higress/plugins/wasm-go v1.4.2 h1:gH7OIGXm4wtW5Vo7L2deMPqF7OVWNESDHv1CaaTGu6s=
github.com/alibaba/higress/plugins/wasm-go v1.4.2/go.mod h1:359don/ahMxpfeLMzr29Cjwcu8IywTTDUzWlBPRNLHw=
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.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
github.com/tidwall/gjson v1.17.3/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,372 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/dashscope"
prompttpl "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/promptTpl"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
// 用于统计函数的递归调用次数
const ToolCallsCount = "ToolCallsCount"
// react的正则规则
const ActionPattern = `Action:\s*(.*?)[.\n]`
const ActionInputPattern = `Action Input:\s*(.*)`
const FinalAnswerPattern = `Final Answer:(.*)`
func main() {
wrapper.SetCtx(
"ai-agent",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
)
}
func parseConfig(gjson gjson.Result, c *PluginConfig, log wrapper.Log) error {
initResponsePromptTpl(gjson, c)
err := initAPIs(gjson, c)
if err != nil {
return err
}
initReActPromptTpl(gjson, c)
initLLMClient(gjson, c)
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
return types.ActionContinue
}
func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
log.Debugf("[onHttpRequestBody] firstreq:%s", prompt)
var userMessage Message
userMessage.Role = "user"
userMessage.Content = prompt
newMessages := []Message{userMessage}
rawRequest.Messages = newMessages
//replace old message and resume request qwen
newbody, err := json.Marshal(rawRequest)
if err != nil {
return types.ActionContinue
} else {
log.Debugf("[onHttpRequestBody] newRequestBody: ", string(newbody))
err := proxywasm.ReplaceHttpRequestBody(newbody)
if err != nil {
log.Debug("替换失败")
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, "替换失败"+err.Error())), -1)
}
log.Debug("[onHttpRequestBody] request替换成功")
return types.ActionContinue
}
}
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
log.Debug("onHttpRequestBody start")
defer log.Debug("onHttpRequestBody end")
//拿到请求
var rawRequest Request
err := json.Unmarshal(body, &rawRequest)
if err != nil {
log.Debugf("[onHttpRequestBody] body json umarshal err: ", err.Error())
return types.ActionContinue
}
log.Debugf("onHttpRequestBody rawRequest: %v", rawRequest)
//获取用户query
var query string
messageLength := len(rawRequest.Messages)
log.Debugf("[onHttpRequestBody] messageLength: %s\n", messageLength)
if messageLength > 0 {
query = rawRequest.Messages[messageLength-1].Content
log.Debugf("[onHttpRequestBody] query: %s\n", query)
} else {
return types.ActionContinue
}
if query == "" {
log.Debug("parse query from request body failed")
return types.ActionContinue
}
//拼装agent prompt模板
tool_desc := make([]string, 0)
tool_names := make([]string, 0)
for _, apiParam := range config.APIParam {
for _, tool_param := range apiParam.Tool_Param {
tool_desc = append(tool_desc, fmt.Sprintf(prompttpl.TOOL_DESC, tool_param.ToolName, tool_param.Description, tool_param.Description, tool_param.Description, tool_param.Parameter), "\n")
tool_names = append(tool_names, tool_param.ToolName)
}
}
var prompt string
if config.PromptTemplate.Language == "CH" {
prompt = fmt.Sprintf(prompttpl.CH_Template,
tool_desc,
config.PromptTemplate.CHTemplate.Question,
config.PromptTemplate.CHTemplate.Thought1,
tool_names,
config.PromptTemplate.CHTemplate.ActionInput,
config.PromptTemplate.CHTemplate.Observation,
config.PromptTemplate.CHTemplate.Thought2,
config.PromptTemplate.CHTemplate.FinalAnswer,
config.PromptTemplate.CHTemplate.Begin,
query)
} else {
prompt = fmt.Sprintf(prompttpl.EN_Template,
tool_desc,
config.PromptTemplate.ENTemplate.Question,
config.PromptTemplate.ENTemplate.Thought1,
tool_names,
config.PromptTemplate.ENTemplate.ActionInput,
config.PromptTemplate.ENTemplate.Observation,
config.PromptTemplate.ENTemplate.Thought2,
config.PromptTemplate.ENTemplate.FinalAnswer,
config.PromptTemplate.ENTemplate.Begin,
query)
}
ctx.SetContext(ToolCallsCount, 0)
//清理历史对话记录
dashscope.MessageStore.Clear()
//将请求加入到历史对话存储器中
dashscope.MessageStore.AddForUser(prompt)
//开始第一次请求
ret := firstReq(config, prompt, rawRequest, log)
return ret
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
log.Debug("onHttpResponseHeaders start")
defer log.Debug("onHttpResponseHeaders end")
return types.ActionContinue
}
func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
if statusCode != http.StatusOK {
log.Debugf("statusCode: %d\n", statusCode)
}
log.Info("========函数返回结果========")
log.Infof(string(responseBody))
observation := "Observation: " + string(responseBody)
dashscope.MessageStore.AddForUser(observation)
completion := dashscope.Completion{
Model: config.LLMInfo.Model,
Messages: dashscope.MessageStore,
MaxTokens: config.LLMInfo.MaxTokens,
}
headers := [][2]string{{"Content-Type", "application/json"}, {"Authorization", "Bearer " + config.LLMInfo.APIKey}}
completionSerialized, _ := json.Marshal(completion)
err := config.LLMClient.Post(
config.LLMInfo.Path,
headers,
completionSerialized,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
//得到gpt的返回结果
var responseCompletion dashscope.CompletionResponse
_ = json.Unmarshal(responseBody, &responseCompletion)
log.Infof("[toolsCall] content: %s\n", responseCompletion.Choices[0].Message.Content)
if responseCompletion.Choices[0].Message.Content != "" {
retType := toolsCall(ctx, config, responseCompletion.Choices[0].Message.Content, rawResponse, log)
if retType == types.ActionContinue {
//得到了Final Answer
var assistantMessage Message
assistantMessage.Role = "assistant"
startIndex := strings.Index(responseCompletion.Choices[0].Message.Content, "Final Answer:")
if startIndex != -1 {
startIndex += len("Final Answer:") // 移动到"Final Answer:"之后的位置
extractedText := responseCompletion.Choices[0].Message.Content[startIndex:]
assistantMessage.Content = extractedText
}
rawResponse.Choices[0].Message = assistantMessage
newbody, err := json.Marshal(rawResponse)
if err != nil {
proxywasm.ResumeHttpResponse()
return
} else {
log.Infof("[onHttpResponseBody] newResponseBody: ", string(newbody))
proxywasm.ReplaceHttpResponseBody(newbody)
log.Debug("[onHttpResponseBody] response替换成功")
proxywasm.ResumeHttpResponse()
}
}
} else {
proxywasm.ResumeHttpRequest()
}
}, uint32(config.LLMInfo.MaxExecutionTime))
if err != nil {
log.Debugf("[onHttpRequestBody] completion err: %s", err.Error())
proxywasm.ResumeHttpRequest()
}
}
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) types.Action {
dashscope.MessageStore.AddForAssistant(content)
//得到最终答案
regexPattern := regexp.MustCompile(FinalAnswerPattern)
finalAnswer := regexPattern.FindStringSubmatch(content)
if len(finalAnswer) > 1 {
return types.ActionContinue
}
count := ctx.GetContext(ToolCallsCount).(int)
count++
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d\n", count, config.LLMInfo.MaxIterations)
//函数递归调用次数,达到了预设的循环次数,强制结束
if int64(count) > config.LLMInfo.MaxIterations {
ctx.SetContext(ToolCallsCount, 0)
return types.ActionContinue
} else {
ctx.SetContext(ToolCallsCount, count)
}
//没得到最终答案
regexAction := regexp.MustCompile(ActionPattern)
regexActionInput := regexp.MustCompile(ActionInputPattern)
action := regexAction.FindStringSubmatch(content)
actionInput := regexActionInput.FindStringSubmatch(content)
if len(action) > 1 && len(actionInput) > 1 {
var url string
var headers [][2]string
var apiClient wrapper.HttpClient
var method string
var reqBody []byte
var key string
for i, apiParam := range config.APIParam {
for _, tool_param := range apiParam.Tool_Param {
if action[1] == tool_param.ToolName {
log.Infof("calls %s\n", tool_param.ToolName)
log.Infof("actionInput[1]: %s", actionInput[1])
//将大模型需要的参数反序列化
var data map[string]interface{}
if err := json.Unmarshal([]byte(actionInput[1]), &data); err != nil {
log.Debugf("Error: %s\n", err.Error())
return types.ActionContinue
}
method = tool_param.Method
//key or header组装
if apiParam.APIKey.Name != "" {
if apiParam.APIKey.In == "query" { //query类型的key要放到url中
headers = nil
key = "?" + apiParam.APIKey.Name + "=" + apiParam.APIKey.Value
} else if apiParam.APIKey.In == "header" { //header类型的key放在header中
headers = [][2]string{{"Content-Type", "application/json"}, {"Authorization", apiParam.APIKey.Name + " " + apiParam.APIKey.Value}}
}
}
if method == "GET" {
//query组装
var args string
for i, param := range tool_param.ParamName { //从参数列表中取出参数
if i == 0 && apiParam.APIKey.In != "query" {
args = "?" + param + "=%s"
args = fmt.Sprintf(args, data[param])
} else {
args = args + "&" + param + "=%s"
args = fmt.Sprintf(args, data[param])
}
}
//url组装
url = apiParam.URL + tool_param.Path + key + args
} else if method == "POST" {
reqBody = nil
//json参数组装
jsonData, err := json.Marshal(data)
if err != nil {
log.Debugf("Error: %s\n", err.Error())
return types.ActionContinue
}
reqBody = jsonData
//url组装
url = apiParam.URL + tool_param.Path + key
}
log.Infof("url: %s\n", url)
apiClient = config.APIClient[i]
break
}
}
}
if apiClient != nil {
err := apiClient.Call(
method,
url,
headers,
reqBody,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
toolsCallResult(ctx, config, content, rawResponse, log, statusCode, responseBody)
}, 50000)
if err != nil {
log.Debugf("tool calls error: %s\n", err.Error())
proxywasm.ResumeHttpRequest()
}
} else {
return types.ActionContinue
}
}
return types.ActionPause
}
// 从response接收到firstreq的大模型返回
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
log.Debugf("onHttpResponseBody start")
defer log.Debugf("onHttpResponseBody end")
//初始化接收gpt返回内容的结构体
var rawResponse Response
err := json.Unmarshal(body, &rawResponse)
if err != nil {
log.Debugf("[onHttpResponseBody] body to json err: %s", err.Error())
return types.ActionContinue
}
log.Infof("first content: %s\n", rawResponse.Choices[0].Message.Content)
//如果gpt返回的内容不是空的
if rawResponse.Choices[0].Message.Content != "" {
//进入agent的循环思考工具调用的过程中
return toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
} else {
return types.ActionContinue
}
}

View File

@@ -0,0 +1,93 @@
package prompttpl
// input param
// {name_for_model}
// {description_for_model}
// {description_for_model}
// {description_for_model}
// {parameters}
const TOOL_DESC = `
%s: Call this tool to interact with the %s API. What is the %s API useful for? %s
Parameters:
%s
Format the arguments as a JSON object.`
/*
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
%s
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of %s
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content.
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s
Question: %s
*/
const EN_Template = `
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
%s
Use the following format:
Question: %s
Thought: %s
Action: the action to take, should be one of %s
Action Input: %s
Observation: %s
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: %s
Final Answer: %s
%s
Question: %s
`
/*
尽你所能回答以下问题。你可以使用以下工具:
%s
请使用以下格式其中Action字段后必须跟着Action Input字段并且不要将Action Input替换成Input或者tool等字段不能出现格式以外的字段名每个字段在每个轮次只出现一次
Question: 你需要回答的输入问题
Thought: 你应该总是思考该做什么
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
Action Input: 行动的输入必须出现在Action后。
Observation: 行动的结果
...这个Thought/Action/Action Input/Observation可以重复N次
Thought: 我现在知道最终答案
Final Answer: 对原始输入问题的最终答案
再次重申,不要修改以上模板的字段名称,开始吧!
Question: %s
*/
const CH_Template = `
尽你所能回答以下问题。你可以使用以下工具:
%s
请使用以下格式其中Action字段后必须跟着Action Input字段并且不要将Action Input替换成Input或者tool等字段不能出现格式以外的字段名每个字段在每个轮次只出现一次
Question: %s
Thought: %s
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
Action Input: %s
Observation: %s
...这个Thought/Action/Action Input/Observation可以重复N次
Thought: %s
Final Answer: %s
%s
Question: %s
`

View File

@@ -0,0 +1 @@
EXTRA_TAGS=proxy_wasm_version_0_2_100

View File

@@ -32,3 +32,15 @@ redis:
serviceName: my-redis.dns
timeout: 2000
```
## 进阶用法
当前默认的缓存 key 是基于 GJSON PATH 的表达式:`messages.@reverse.0.content` 提取,含义是把 messages 数组反转后取第一项的 content
GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key可以写成 `messages.@reverse.#(role=="user").content`
如果希望将所有 role 为 user 的 content 拼成一个数组作为 key可以写成`messages.@reverse.#(role=="user")#.content`
还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key可以写成`messages.@reverse.#(role=="user")#.content|1`
更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。

View File

@@ -8,7 +8,7 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240528060522-53bccf89f441
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
github.com/tidwall/gjson v1.14.3
github.com/tidwall/resp v0.1.1
github.com/tidwall/sjson v1.2.5

View File

@@ -5,6 +5,7 @@ github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbG
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-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@@ -30,6 +30,7 @@ func main() {
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
wrapper.ProcessStreamingResponseBodyBy(onHttpResponseBody),
)
}
@@ -54,9 +55,16 @@ func main() {
// cacheKeyFrom:
// requestBody: "messages.@reverse.0.content"
// cacheValueFrom:
// responseBody: "choices.0"
// responseBody: "choices.0.message.content"
// cacheStreamValueFrom:
// responseBody: "choices.0.delta.content"
// returnResponseTemplate: |
// {"id":"from-cache","choices":[%s],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
// {"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
// returnStreamResponseTemplate: |
// data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
//
// data:[DONE]
//
// @End
type RedisInfo struct {
@@ -174,12 +182,6 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
ctx.DontReadRequestBody()
return types.ActionContinue
}
// compatiable with qwen
x_dashscope_sse, _ := proxywasm.GetHttpRequestHeader("X-DashScope-SSE")
accept, _ := proxywasm.GetHttpRequestHeader("Accept")
if x_dashscope_sse == "enable" || strings.Contains(accept, "text/event-stream") {
ctx.SetContext(StreamContextKey, struct{}{})
}
proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
// The request has a body and requires delaying the header transmission until a cache miss occurs,
// at which point the header should be sent.
@@ -220,9 +222,9 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
log.Debugf("cache hit, key:%s", key)
ctx.SetContext(CacheKeyContextKey, nil)
if !stream {
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, response.String())), -1)
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, response.String())), -1)
} else {
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnStreamResponseTemplate, response.String())), -1)
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnStreamResponseTemplate, response.String())), -1)
}
})
if err != nil {
@@ -267,6 +269,14 @@ func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage
return ""
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
if strings.Contains(contentType, "text/event-stream") {
ctx.SetContext(StreamContextKey, struct{}{})
}
return types.ActionContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
if ctx.GetContext(ToolCallsContextKey) != nil {
// we should not cache tool call result

View File

@@ -0,0 +1,3 @@
config.yaml
main.wasm
tmp/

Some files were not shown because too many files have changed in this diff Show More