mirror of
https://github.com/alibaba/higress.git
synced 2026-02-27 14:10:51 +08:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7697af9d2b | ||
|
|
3660715506 | ||
|
|
7bd438877b | ||
|
|
0fbeb39cac | ||
|
|
d02c974af4 | ||
|
|
8ad4970231 | ||
|
|
aee37c5e22 | ||
|
|
73cf32aadd | ||
|
|
1ab69fcf82 | ||
|
|
9b995321bb | ||
|
|
00cac813e3 | ||
|
|
548cf2f081 | ||
|
|
c1f2504e87 | ||
|
|
7e8b0445ad | ||
|
|
63d5422da6 | ||
|
|
035e81a5ca | ||
|
|
9a1edcd4c8 | ||
|
|
2219a17898 | ||
|
|
93c1e5c2bb | ||
|
|
7c2d2b2855 | ||
|
|
b1550e91ab | ||
|
|
0b42836e85 | ||
|
|
7c33ebf6ea | ||
|
|
acec48ed8b | ||
|
|
d309bf2e25 | ||
|
|
496d365a95 | ||
|
|
d952fa562b | ||
|
|
e7561c30e5 | ||
|
|
cdd71155a9 | ||
|
|
a5ccb90b28 | ||
|
|
d76f574ab3 | ||
|
|
bb6c43c767 | ||
|
|
b8f5826a32 | ||
|
|
0d79386ce2 | ||
|
|
871ae179c3 | ||
|
|
f8d62a8ac3 | ||
|
|
badf4b7101 | ||
|
|
fc6902ded2 | ||
|
|
d96994767c | ||
|
|
32e5a59ae0 | ||
|
|
49bb5ec2b9 | ||
|
|
11ff2d1d31 | ||
|
|
c67f494b49 | ||
|
|
299621476f | ||
|
|
7e6168a644 | ||
|
|
e923cbaecc | ||
|
|
6f86c31bac | ||
|
|
51c956f0b3 | ||
|
|
d0693d8c4b | ||
|
|
e298078065 | ||
|
|
85f8eb5166 | ||
|
|
0a112d1a1e | ||
|
|
04ce776f14 | ||
|
|
952c9ec5dc | ||
|
|
1a53c7b4d3 | ||
|
|
ae6dab919d | ||
|
|
601b205abc | ||
|
|
9972e7611a | ||
|
|
c30ca5dd9e | ||
|
|
e26a2a37d7 | ||
|
|
f20c48e960 | ||
|
|
e126f3a888 | ||
|
|
93317adbc7 | ||
|
|
ecf52aecfc | ||
|
|
3ed28f2a66 | ||
|
|
4d0d8a7f50 | ||
|
|
1f8d50c0b1 | ||
|
|
14b11dcb05 | ||
|
|
71aae9ddf6 | ||
|
|
1b119ed371 | ||
|
|
ea99159d51 | ||
|
|
567d7c25f3 | ||
|
|
708e7af79a | ||
|
|
260772926c | ||
|
|
af4e34b7ed | ||
|
|
8293042c25 | ||
|
|
1acaaea222 | ||
|
|
e004321cb0 | ||
|
|
b82853c653 | ||
|
|
bef9139753 | ||
|
|
dc61bfc5c5 | ||
|
|
b24731593f | ||
|
|
e7761a2ecc | ||
|
|
86239c4a4b | ||
|
|
c923e5cb42 | ||
|
|
ee67553816 | ||
|
|
ffc5458a91 | ||
|
|
55f6ed7953 | ||
|
|
9e5188cfca | ||
|
|
f51408d7ff | ||
|
|
0471249e7f | ||
|
|
59fe661cd2 | ||
|
|
7610c9f504 |
@@ -3,9 +3,16 @@ name: Build and Push Wasm Plugin Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "wasm-go-*-v*.*.*" # 匹配 wasm-go-{pluginName}-vX.Y.Z 格式的标签
|
||||
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
plugin_type:
|
||||
description: 'Type of the plugin'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- go
|
||||
- rust
|
||||
plugin_name:
|
||||
description: 'Name of the plugin'
|
||||
required: true
|
||||
@@ -23,32 +30,40 @@ jobs:
|
||||
env:
|
||||
IMAGE_REGISTRY_SERVICE: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
IMAGE_REPOSITORY: ${{ vars.PLUGIN_IMAGE_REPOSITORY || 'plugins' }}
|
||||
RUST_VERSION: 1.82
|
||||
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
|
||||
- name: Set plugin_type, plugin_name and version from inputs or ref_name
|
||||
id: set_vars
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
plugin_type="${{ github.event.inputs.plugin_type }}"
|
||||
plugin_name="${{ github.event.inputs.plugin_name }}"
|
||||
version="${{ github.event.inputs.version }}"
|
||||
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-rust-builder:rust${{ env.RUST_VERSION }}-oras${{ env.ORAS_VERSION }}"
|
||||
else
|
||||
ref_name=${{ github.ref_name }}
|
||||
plugin_type=${ref_name#*-} # 删除插件类型前面的字段(wasm-)
|
||||
plugin_type=${plugin_type%-*} # 删除插件类型后面的字段(-{plugin_name}-vX.Y.Z)
|
||||
plugin_name=${ref_name#*-*-} # 删除插件名前面的字段(wasm-go-)
|
||||
plugin_name=${plugin_name%-*} # 删除插件名后面的字段(-vX.Y.Z)
|
||||
version=$(echo "$ref_name" | awk -F'v' '{print $2}')
|
||||
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 }}"
|
||||
fi
|
||||
|
||||
echo "PLUGIN_TYPE=$plugin_type" >> $GITHUB_ENV
|
||||
echo "PLUGIN_NAME=$plugin_name" >> $GITHUB_ENV
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
echo "BUILDER_IMAGE=$builder_image" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: File Check
|
||||
run: |
|
||||
workspace=${{ github.workspace }}/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
workspace=${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
|
||||
push_command="./plugin.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
|
||||
# 查找spec.yaml
|
||||
@@ -75,10 +90,10 @@ jobs:
|
||||
|
||||
echo "PUSH_COMMAND=\"$push_command\"" >> $GITHUB_ENV
|
||||
|
||||
- name: Run a wasm-go-builder
|
||||
- name: Run a wasm-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 }}
|
||||
BUILDER_IMAGE: ${{ env.BUILDER_IMAGE }}
|
||||
run: |
|
||||
docker run -itd --name builder -v ${{ github.workspace }}:/workspace -e PLUGIN_NAME=${{ env.PLUGIN_NAME }} --rm ${{ env.BUILDER_IMAGE }} /bin/bash
|
||||
|
||||
@@ -89,9 +104,11 @@ jobs:
|
||||
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号,确保oras push正常解析
|
||||
|
||||
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
|
||||
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:latest"
|
||||
echo "TargetImage=${target_image}"
|
||||
echo "TargetImageLatest=${target_image_latest}"
|
||||
|
||||
cd ${{ github.workspace }}/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
cd ${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
|
||||
if [ -f ./.buildrc ]; then
|
||||
echo 'Found .buildrc file, sourcing it...'
|
||||
. ./.buildrc
|
||||
@@ -99,7 +116,7 @@ jobs:
|
||||
echo '.buildrc file not found'
|
||||
fi
|
||||
echo "EXTRA_TAGS=${EXTRA_TAGS}"
|
||||
|
||||
if [ "${PLUGIN_TYPE}" == "go" ]; then
|
||||
command="
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
@@ -108,7 +125,23 @@ jobs:
|
||||
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}
|
||||
oras push ${target_image_latest} ${push_command}
|
||||
"
|
||||
docker exec builder bash -c "$command"
|
||||
elif [ "${PLUGIN_TYPE}" == "rust" ]; then
|
||||
command="
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-rust/extensions/${PLUGIN_NAME}
|
||||
cargo build --target wasm32-wasi --release
|
||||
cp target/wasm32-wasi/release/*.wasm plugin.wasm
|
||||
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}
|
||||
oras push ${target_image_latest} ${push_command}
|
||||
"
|
||||
else
|
||||
|
||||
|
||||
command="
|
||||
echo "unkown type ${PLUGIN_TYPE}"
|
||||
"
|
||||
fi
|
||||
docker exec builder bash -c "$command"
|
||||
|
||||
24
.github/workflows/release-crd.yaml
vendored
Normal file
24
.github/workflows/release-crd.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Release CRD to GitHub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
release-crd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: generate crds
|
||||
run: |
|
||||
cat helm/core/crds/customresourcedefinitions.gen.yaml helm/core/crds/istio-envoyfilter.yaml > crd.yaml
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
crd.yaml
|
||||
@@ -3,7 +3,7 @@
|
||||
/istio @SpecialYang @johnlanni
|
||||
/pkg @SpecialYang @johnlanni @CH3CHO
|
||||
/plugins @johnlanni @WeixinX @CH3CHO
|
||||
/plugins/wasm-rust @007gzs
|
||||
/plugins/wasm-rust @007gzs @jizhuozhi
|
||||
/registry @NameHaibinZhang @2456868764 @johnlanni
|
||||
/test @Xunzhuo @2456868764 @CH3CHO
|
||||
/tools @johnlanni @Xunzhuo @2456868764
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing to Higress
|
||||
|
||||
It is warmly welcomed if you have interest to hack on Higress. First, we encourage this kind of willing very much. And here is a list of contributing guide for you.
|
||||
Your interest in contributing to Higress is warmly welcomed. First, we encourage this kind of willing very much. And here is a list of contributing guide for you.
|
||||
|
||||
[[中文贡献文档](./CONTRIBUTING_CN.md)]
|
||||
|
||||
|
||||
195
CONTRIBUTING_JP.md
Normal file
195
CONTRIBUTING_JP.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Higress への貢献
|
||||
|
||||
Higress のハッキングに興味がある場合は、温かく歓迎します。まず、このような意欲を非常に奨励します。そして、以下は貢献ガイドのリストです。
|
||||
|
||||
[[中文](./CONTRIBUTING.md)] | [[English Contributing Document](./CONTRIBUTING_EN.md)]
|
||||
|
||||
## トピック
|
||||
|
||||
- [Higress への貢献](#higress-への貢献)
|
||||
- [トピック](#トピック)
|
||||
- [セキュリティ問題の報告](#セキュリティ問題の報告)
|
||||
- [一般的な問題の報告](#一般的な問題の報告)
|
||||
- [コードとドキュメントの貢献](#コードとドキュメントの貢献)
|
||||
- [ワークスペースの準備](#ワークスペースの準備)
|
||||
- [ブランチの定義](#ブランチの定義)
|
||||
- [コミットルール](#コミットルール)
|
||||
- [コミットメッセージ](#コミットメッセージ)
|
||||
- [コミット内容](#コミット内容)
|
||||
- [PR 説明](#pr-説明)
|
||||
- [テストケースの貢献](#テストケースの貢献)
|
||||
- [何かを手伝うための参加](#何かを手伝うための参加)
|
||||
- [コードスタイル](#コードスタイル)
|
||||
|
||||
## セキュリティ問題の報告
|
||||
|
||||
セキュリティ問題は常に真剣に扱われます。通常の原則として、セキュリティ問題を広めることは推奨しません。Higress のセキュリティ問題を発見した場合は、公開で議論せず、公開の問題を開かないでください。代わりに、[higress@googlegroups.com](mailto:higress@googlegroups.com) にプライベートなメールを送信して報告することをお勧めします。
|
||||
|
||||
## 一般的な問題の報告
|
||||
|
||||
正直なところ、Higress のすべてのユーザーを非常に親切な貢献者と見なしています。Higress を体験した後、プロジェクトに対するフィードバックがあるかもしれません。その場合は、[NEW ISSUE](https://github.com/alibaba/higress/issues/new/choose) を通じて問題を開くことを自由に行ってください。
|
||||
|
||||
Higress プロジェクトを分散型で協力しているため、**よく書かれた**、**詳細な**、**明確な**問題報告を高く評価します。コミュニケーションをより効率的にするために、問題が検索リストに存在するかどうかを検索することを希望します。存在する場合は、新しい問題を開くのではなく、既存の問題のコメントに詳細を追加してください。
|
||||
|
||||
問題の詳細をできるだけ標準化するために、問題報告者のために [ISSUE TEMPLATE](./.github/ISSUE_TEMPLATE) を設定しました。テンプレートのフィールドに従って指示に従って記入してください。
|
||||
|
||||
問題を開く場合は多くのケースがあります:
|
||||
|
||||
* バグ報告
|
||||
* 機能要求
|
||||
* パフォーマンス問題
|
||||
* 機能提案
|
||||
* 機能設計
|
||||
* 助けが必要
|
||||
* ドキュメントが不完全
|
||||
* テストの改善
|
||||
* プロジェクトに関する質問
|
||||
* その他
|
||||
|
||||
また、新しい問題を記入する際には、投稿から機密データを削除することを忘れないでください。機密データには、パスワード、秘密鍵、ネットワークの場所、プライベートなビジネスデータなどが含まれる可能性があります。
|
||||
|
||||
## コードとドキュメントの貢献
|
||||
|
||||
Higress プロジェクトをより良くするためのすべての行動が奨励されます。GitHub では、Higress のすべての改善は PR(プルリクエストの略)を通じて行うことができます。
|
||||
|
||||
* タイプミスを見つけた場合は、修正してみてください!
|
||||
* バグを見つけた場合は、修正してみてください!
|
||||
* 冗長なコードを見つけた場合は、削除してみてください!
|
||||
* 欠落しているテストケースを見つけた場合は、追加してみてください!
|
||||
* 機能を強化できる場合は、**ためらわないでください**!
|
||||
* コードが不明瞭な場合は、コメントを追加して明確にしてください!
|
||||
* コードが醜い場合は、リファクタリングしてみてください!
|
||||
* ドキュメントの改善に役立つ場合は、さらに良いです!
|
||||
* ドキュメントが不正確な場合は、修正してください!
|
||||
* ...
|
||||
|
||||
実際には、それらを完全にリストすることは不可能です。1つの原則を覚えておいてください:
|
||||
|
||||
> あなたからの PR を楽しみにしています。
|
||||
|
||||
Higress を PR で改善する準備ができたら、ここで PR ルールを確認することをお勧めします。
|
||||
|
||||
* [ワークスペースの準備](#ワークスペースの準備)
|
||||
* [ブランチの定義](#ブランチの定義)
|
||||
* [コミットルール](#コミットルール)
|
||||
* [PR 説明](#pr-説明)
|
||||
|
||||
### ワークスペースの準備
|
||||
|
||||
PR を提出するために、GitHub ID に登録していることを前提とします。その後、以下の手順で準備を完了できます:
|
||||
|
||||
1. Higress を自分のリポジトリに **FORK** します。この作業を行うには、[alibaba/higress](https://github.com/alibaba/higress) のメインページの右上にある Fork ボタンをクリックするだけです。その後、`https://github.com/<your-username>/higress` に自分のリポジトリが作成されます。ここで、`your-username` はあなたの GitHub ユーザー名です。
|
||||
|
||||
2. 自分のリポジトリをローカルに **CLONE** します。`git clone git@github.com:<your-username>/higress.git` を使用してリポジトリをローカルマシンにクローンします。その後、新しいブランチを作成して、行いたい変更を完了できます。
|
||||
|
||||
3. リモートを `git@github.com:alibaba/higress.git` に設定します。以下の2つのコマンドを使用します:
|
||||
|
||||
```bash
|
||||
git remote add upstream git@github.com:alibaba/higress.git
|
||||
git remote set-url --push upstream no-pushing
|
||||
```
|
||||
|
||||
このリモート設定を使用すると、git リモート設定を次のように確認できます:
|
||||
|
||||
```shell
|
||||
$ git remote -v
|
||||
origin git@github.com:<your-username>/higress.git (fetch)
|
||||
origin git@github.com:<your-username>/higress.git (push)
|
||||
upstream git@github.com:alibaba/higress.git (fetch)
|
||||
upstream no-pushing (push)
|
||||
```
|
||||
|
||||
これを追加すると、ローカルブランチを上流ブランチと簡単に同期できます。
|
||||
|
||||
### ブランチの定義
|
||||
|
||||
現在、プルリクエストを通じたすべての貢献は Higress の [main ブランチ](https://github.com/alibaba/higress/tree/main) に対するものであると仮定します。貢献する前に、ブランチの定義を理解することは非常に役立ちます。
|
||||
|
||||
貢献者として、プルリクエストを通じたすべての貢献は main ブランチに対するものであることを再度覚えておいてください。Higress プロジェクトには、リリースブランチ(例:0.6.0、0.6.1)、機能ブランチ、ホットフィックスブランチなど、いくつかの他のブランチがあります。
|
||||
|
||||
正式にバージョンをリリースする際には、リリースブランチが作成され、バージョン番号で命名されます。
|
||||
|
||||
リリース後、リリースブランチのコミットを main ブランチにマージします。
|
||||
|
||||
特定のバージョンにバグがある場合、後のバージョンで修正するか、特定のホットフィックスバージョンで修正するかを決定します。ホットフィックスバージョンで修正することを決定した場合、対応するリリースブランチに基づいてホットフィックスブランチをチェックアウトし、コード修正と検証を行い、main ブランチにマージします。
|
||||
|
||||
大きな機能については、開発と検証のために機能ブランチを引き出します。
|
||||
|
||||
### コミットルール
|
||||
|
||||
実際には、Higress ではコミット時に2つのルールを真剣に考えています:
|
||||
|
||||
* [コミットメッセージ](#コミットメッセージ)
|
||||
* [コミット内容](#コミット内容)
|
||||
|
||||
#### コミットメッセージ
|
||||
|
||||
コミットメッセージは、提出された PR の目的をレビュアーがよりよく理解するのに役立ちます。また、コードレビューの手続きを加速するのにも役立ちます。貢献者には、曖昧なメッセージではなく、**明確な**コミットメッセージを使用することを奨励します。一般的に、以下のコミットメッセージタイプを推奨します:
|
||||
|
||||
* docs: xxxx. 例:"docs: add docs about Higress cluster installation".
|
||||
* feature: xxxx. 例:"feature: use higress config instead of istio config".
|
||||
* bugfix: xxxx. 例:"bugfix: fix panic when input nil parameter".
|
||||
* refactor: xxxx. 例:"refactor: simplify to make codes more readable".
|
||||
* test: xxx. 例:"test: add unit test case for func InsertIntoArray".
|
||||
* その他の読みやすく明確な表現方法。
|
||||
|
||||
一方で、以下のような方法でのコミットメッセージは推奨しません:
|
||||
|
||||
* ~~バグ修正~~
|
||||
* ~~更新~~
|
||||
* ~~ドキュメント追加~~
|
||||
|
||||
迷った場合は、[Git コミットメッセージの書き方](http://chris.beams.io/posts/git-commit/) を参照してください。
|
||||
|
||||
#### コミット内容
|
||||
|
||||
コミット内容は、1つのコミットに含まれるすべての内容の変更を表します。1つのコミットに、他のコミットの助けを借りずにレビュアーが完全にレビューできる内容を含めるのが最善です。言い換えれば、1つのコミットの内容は CI を通過でき、コードの混乱を避けることができます。簡単に言えば、次の3つの小さなルールを覚えておく必要があります:
|
||||
|
||||
* コミットで非常に大きな変更を避ける;
|
||||
* 各コミットが完全でレビュー可能であること。
|
||||
* コミット時に git config(`user.name`、`user.email`)を確認して、それが GitHub ID に関連付けられていることを確認します。
|
||||
|
||||
```bash
|
||||
git config --get user.name
|
||||
git config --get user.email
|
||||
```
|
||||
|
||||
* pr を提出する際には、'changes/' フォルダーの下の XXX.md ファイルに現在の変更の簡単な説明を追加してください。
|
||||
|
||||
さらに、コード変更部分では、すべての貢献者が Higress の [コードスタイル](#コードスタイル) を読むことをお勧めします。
|
||||
|
||||
コミットメッセージやコミット内容に関係なく、コードレビューに重点を置いています。
|
||||
|
||||
### PR 説明
|
||||
|
||||
PR は Higress プロジェクトファイルを変更する唯一の方法です。レビュアーが目的をよりよく理解できるようにするために、PR 説明は詳細すぎることはありません。貢献者には、[PR テンプレート](./.github/PULL_REQUEST_TEMPLATE.md) に従ってプルリクエストを完了することを奨励します。
|
||||
|
||||
### 開発前の準備
|
||||
|
||||
```shell
|
||||
make prebuild && go mod tidy
|
||||
```
|
||||
|
||||
## テストケースの貢献
|
||||
|
||||
テストケースは歓迎されます。現在、Higress の機能テストケースが高優先度です。
|
||||
|
||||
* 単体テストの場合、同じモジュールの test ディレクトリに xxxTest.go という名前のテストファイルを作成する必要があります。
|
||||
* 統合テストの場合、統合テストを test ディレクトリに配置できます。
|
||||
//TBD
|
||||
|
||||
## 何かを手伝うための参加
|
||||
|
||||
GitHub を Higress の協力の主要な場所として選択しました。したがって、Higress の最新の更新は常にここにあります。PR を通じた貢献は明確な助けの方法ですが、他の方法も呼びかけています。
|
||||
|
||||
* 可能であれば、他の人の質問に返信する;
|
||||
* 他のユーザーの問題を解決するのを手伝う;
|
||||
* 他の人の PR 設計をレビューするのを手伝う;
|
||||
* 他の人の PR のコードをレビューするのを手伝う;
|
||||
* Higress について議論して、物事を明確にする;
|
||||
* GitHub 以外で Higress 技術を宣伝する;
|
||||
* Higress に関するブログを書くなど。
|
||||
|
||||
## コードスタイル
|
||||
//TBD
|
||||
要するに、**どんな助けも貢献です。**
|
||||
@@ -72,17 +72,17 @@ go.test.coverage: prebuild
|
||||
|
||||
.PHONY: build
|
||||
build: prebuild $(OUT)
|
||||
GOPROXY=$(GOPROXY) GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HIGRESS_BINARIES)
|
||||
GOPROXY="$(GOPROXY)" GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HIGRESS_BINARIES)
|
||||
|
||||
.PHONY: build-linux
|
||||
build-linux: prebuild $(OUT)
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HIGRESS_BINARIES)
|
||||
GOPROXY="$(GOPROXY)" GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HIGRESS_BINARIES)
|
||||
|
||||
$(AMD64_OUT_LINUX)/higress:
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_amd64/ $(HIGRESS_BINARIES)
|
||||
GOPROXY="$(GOPROXY)" GOOS=linux GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_amd64/ $(HIGRESS_BINARIES)
|
||||
|
||||
$(ARM64_OUT_LINUX)/higress:
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
|
||||
GOPROXY="$(GOPROXY)" GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
|
||||
|
||||
.PHONY: build-hgctl
|
||||
build-hgctl: prebuild $(OUT)
|
||||
@@ -187,8 +187,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 ?= 7722dc383cd5d566d1b10a738f79a4cba1a18304
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 7722dc383cd5d566d1b10a738f79a4cba1a18304
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 2.0.1
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 2.0.1
|
||||
|
||||
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'
|
||||
@@ -221,11 +221,15 @@ clean-higress: ## Cleans all the intermediate files and folders previously gener
|
||||
rm -rf $(DIRS_TO_CLEAN)
|
||||
|
||||
clean-istio:
|
||||
rm -rf external/api
|
||||
rm -rf external/client-go
|
||||
rm -rf external/istio
|
||||
rm -rf external/pkg
|
||||
|
||||
clean-gateway: clean-istio
|
||||
rm -rf external/envoy
|
||||
rm -rf external/proxy
|
||||
rm -rf external/go-control-plane
|
||||
rm -rf external/package/envoy.tar.gz
|
||||
|
||||
clean-env:
|
||||
@@ -284,6 +288,8 @@ delete-cluster: $(tools/kind) ## Delete kind cluster.
|
||||
.PHONY: kube-load-image
|
||||
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/dubbo-provider-demo 0.0.3-x86
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/nacos-standlone-rc3 1.0.0-RC3
|
||||
tools/hack/docker-pull-image.sh docker.io/hashicorp/consul 1.16.0
|
||||
@@ -294,6 +300,7 @@ kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster us
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server v1.0
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-body 1.0.0
|
||||
tools/hack/docker-pull-image.sh openpolicyagent/opa latest
|
||||
tools/hack/docker-pull-image.sh curlimages/curl latest
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/dubbo-provider-demo 0.0.3-x86
|
||||
@@ -306,6 +313,7 @@ kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster us
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server v1.0
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-body 1.0.0
|
||||
tools/hack/kind-load-image.sh openpolicyagent/opa latest
|
||||
tools/hack/kind-load-image.sh curlimages/curl latest
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
|
||||
|
||||
|
||||
51
README.md
51
README.md
@@ -1,3 +1,4 @@
|
||||
<a name="readme-top"></a>
|
||||
<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>
|
||||
@@ -9,7 +10,7 @@
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
[**官网**](https://higress.cn/) |
|
||||
[**文档**](https://higress.cn/docs/latest/user/quickstart/) |
|
||||
[**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) |
|
||||
[**博客**](https://higress.cn/blog/) |
|
||||
[**电子书**](https://higress.cn/docs/ebook/wasm14/) |
|
||||
[**开发指引**](https://higress.cn/docs/latest/dev/architecture/) |
|
||||
@@ -17,18 +18,19 @@
|
||||
|
||||
|
||||
<p>
|
||||
<a href="README_EN.md"> English <a/> | 中文
|
||||
<a href="README_EN.md"> English <a/>| 中文 | <a href="README_JP.md"> 日本語 <a/>
|
||||
</p>
|
||||
|
||||
|
||||
Higress 是基于阿里内部多年的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。
|
||||
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy,可以用 Go/Rust/JS 等编写 Wasm 插件,提供了数十个现成的通用插件,以及开箱即用的控制台(demo 点[这里](http://demo.higress.io/))
|
||||
|
||||
Higress 在阿里内部作为 AI 网关,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
|
||||
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
|
||||
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
|
||||

|
||||
Higress 基于 AI 网关能力,支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
|
||||
|
||||

|
||||
|
||||
|
||||
## Summary
|
||||
@@ -58,7 +60,7 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
|
||||
- 8080 端口:网关 HTTP 协议入口
|
||||
- 8443 端口:网关 HTTPS 协议入口
|
||||
|
||||
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内不可访问的影响**
|
||||
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
|
||||
|
||||
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
|
||||
|
||||
@@ -67,23 +69,32 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||
- **AI 网关**:
|
||||
|
||||
Higress 提供了一站式的 AI 插件集,可以增强依赖 AI 能力业务的稳定性、灵活性、可观测性,使得业务与 AI 的集成更加便捷和高效。
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
|
||||

|
||||
|
||||
- **Kubernetes Ingress 网关**:
|
||||
|
||||
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
|
||||
|
||||
支持 [Gateway API](https://gateway-api.sigs.k8s.io/) 标准,支持用户从 Ingress API 平滑迁移到 Gateway API。
|
||||
|
||||
相比 ingress-nginx,资源开销大幅下降,路由变更生效速度有十倍提升:
|
||||
|
||||

|
||||

|
||||
|
||||
- **微服务网关**:
|
||||
|
||||
Higress 可以作为微服务网关, 能够对接多种类型的注册中心发现服务配置路由,例如 Nacos, ZooKeeper, Consul, Eureka 等。
|
||||
|
||||
并且深度集成了 [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) 等微服务技术栈,基于 Envoy C++ 网关内核的出色性能,相比传统 Java 类微服务网关,可以显著降低资源使用率,减少成本。
|
||||
|
||||

|
||||
|
||||
- **安全防护网关**:
|
||||
|
||||
Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
|
||||
Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
|
||||
|
||||
## 核心优势
|
||||
|
||||
@@ -165,7 +176,7 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||
### 交流群
|
||||
|
||||

|
||||

|
||||
|
||||
### 技术分享
|
||||
|
||||
@@ -173,3 +184,23 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||

|
||||
|
||||
### 关联仓库
|
||||
|
||||
- Higress 控制台:https://github.com/higress-group/higress-console
|
||||
- Higress(独立运行版):https://github.com/higress-group/higress-standalone
|
||||
|
||||
### 贡献者
|
||||
|
||||
<a href="https://github.com/alibaba/higress/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
|
||||
</a>
|
||||
|
||||
### Star History
|
||||
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ 返回顶部 ↑
|
||||
</a>
|
||||
</p>
|
||||
|
||||
32
README_EN.md
32
README_EN.md
@@ -1,3 +1,4 @@
|
||||
<a name="readme-top"></a>
|
||||
<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>
|
||||
@@ -15,7 +16,7 @@
|
||||
|
||||
|
||||
<p>
|
||||
English | <a href="README.md">中文<a/>
|
||||
English | <a href="README.md">中文<a/> | <a href="README_JP.md">日本語<a/>
|
||||
</p>
|
||||
|
||||
Higress is a cloud-native api gateway based on Alibaba's internal gateway practices.
|
||||
@@ -47,7 +48,7 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
|
||||
|
||||
Higress can function as a microservice gateway, which can discovery microservices from various service registries, such as Nacos, ZooKeeper, Consul, Eureka, etc.
|
||||
|
||||
It deeply integrates of [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
|
||||
It deeply integrates with [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
|
||||
|
||||
- **Security gateway**:
|
||||
|
||||
@@ -57,7 +58,7 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
|
||||
|
||||
- **Easy to use**
|
||||
|
||||
Provide one-stop gateway solutions for traffic scheduling, service management, and security protection, support Console, K8s Ingress, and Gateway API configuration methods, and also support HTTP to Dubbo protocol conversion, and easily complete protocol mapping configuration.
|
||||
Provides one-stop gateway solutions for traffic scheduling, service management, and security protection, support Console, K8s Ingress, and Gateway API configuration methods, and also support HTTP to Dubbo protocol conversion, and easily complete protocol mapping configuration.
|
||||
|
||||
- **Easy to expand**
|
||||
|
||||
@@ -73,7 +74,7 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
|
||||
|
||||
- **Security**
|
||||
|
||||
Provides JWT, OIDC, custom authentication and authentication, deeply integrates open source web application firewall.
|
||||
Provides JWT, OIDC, custom authentication and authentication, deeply integrates open-source web application firewall.
|
||||
|
||||
## Community
|
||||
|
||||
@@ -81,4 +82,25 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
|
||||
|
||||
### Thanks
|
||||
|
||||
Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank-you to Envoy and Istio.
|
||||
Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank you to Envoy and Istio.
|
||||
|
||||
### Related Repositories
|
||||
|
||||
- Higress Console: https://github.com/higress-group/higress-console
|
||||
- Higress Standalone: https://github.com/higress-group/higress-standalone
|
||||
|
||||
### Contributors
|
||||
|
||||
<a href="https://github.com/alibaba/higress/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
|
||||
</a>
|
||||
|
||||
### Star History
|
||||
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ Back to Top ↑
|
||||
</a>
|
||||
</p>
|
||||
206
README_JP.md
Normal file
206
README_JP.md
Normal file
@@ -0,0 +1,206 @@
|
||||
<a name="readme-top"></a>
|
||||
<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>
|
||||
AIゲートウェイ
|
||||
</h1>
|
||||
<h4 align="center"> AIネイティブAPIゲートウェイ </h4>
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
[**公式サイト**](https://higress.cn/) |
|
||||
[**ドキュメント**](https://higress.cn/docs/latest/overview/what-is-higress/) |
|
||||
[**ブログ**](https://higress.cn/blog/) |
|
||||
[**電子書籍**](https://higress.cn/docs/ebook/wasm14/) |
|
||||
[**開発ガイド**](https://higress.cn/docs/latest/dev/architecture/) |
|
||||
[**AIプラグイン**](https://higress.cn/plugin/)
|
||||
|
||||
|
||||
<p>
|
||||
<a href="README_EN.md"> English <a/> | <a href="README.md">中文<a/> | 日本語
|
||||
</p>
|
||||
|
||||
|
||||
Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲートウェイで、Go/Rust/JSなどを使用してWasmプラグインを作成できます。数十の既製の汎用プラグインと、すぐに使用できるコンソールを提供しています(デモは[こちら](http://demo.higress.io/))。
|
||||
|
||||
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。
|
||||
|
||||
Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
|
||||
|
||||
Higressは、AIゲートウェイ機能を基盤に、Tongyi Qianwen APP、Bailian大規模モデルAPI、機械学習PAIプラットフォームなどのAIビジネスをサポートしています。また、国内の主要なAIGC企業(例:ZeroOne)やAI製品(例:FastGPT)にもサービスを提供しています。
|
||||
|
||||

|
||||
|
||||
|
||||
## 目次
|
||||
|
||||
- [**クイックスタート**](#クイックスタート)
|
||||
- [**機能紹介**](#機能紹介)
|
||||
- [**使用シナリオ**](#使用シナリオ)
|
||||
- [**主な利点**](#主な利点)
|
||||
- [**コミュニティ**](#コミュニティ)
|
||||
|
||||
## クイックスタート
|
||||
|
||||
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デプロイなどの他のインストール方法については、公式サイトの[クイックスタートドキュメント](https://higress.cn/docs/latest/user/quickstart/)を参照してください。
|
||||
|
||||
|
||||
## 使用シナリオ
|
||||
|
||||
- **AIゲートウェイ**:
|
||||
|
||||
Higressは、国内外のすべてのLLMモデルプロバイダーと統一されたプロトコルで接続でき、豊富なAI可観測性、多モデル負荷分散/フォールバック、AIトークンフロー制御、AIキャッシュなどの機能を備えています。
|
||||
|
||||

|
||||
|
||||
- **Kubernetes Ingressゲートウェイ**:
|
||||
|
||||
HigressはK8sクラスターのIngressエントリーポイントゲートウェイとして機能し、多くのK8s Nginx Ingressの注釈に対応しています。K8s Nginx IngressからHigressへのスムーズな移行が可能です。
|
||||
|
||||
[Gateway API](https://gateway-api.sigs.k8s.io/)標準をサポートし、ユーザーがIngress APIからGateway APIにスムーズに移行できるようにします。
|
||||
|
||||
ingress-nginxと比較して、リソースの消費が大幅に減少し、ルーティングの変更が10倍速く反映されます。
|
||||
|
||||

|
||||

|
||||
|
||||
- **マイクロサービスゲートウェイ**:
|
||||
|
||||
Higressはマイクロサービスゲートウェイとして機能し、Nacos、ZooKeeper、Consul、Eurekaなどのさまざまなサービスレジストリからサービスを発見し、ルーティングを構成できます。
|
||||
|
||||
また、[Dubbo](https://github.com/apache/dubbo)、[Nacos](https://github.com/alibaba/nacos)、[Sentinel](https://github.com/alibaba/Sentinel)などのマイクロサービス技術スタックと深く統合されています。Envoy C++ゲートウェイコアの優れたパフォーマンスに基づいて、従来のJavaベースのマイクロサービスゲートウェイと比較して、リソース使用率を大幅に削減し、コストを削減できます。
|
||||
|
||||

|
||||
|
||||
- **セキュリティゲートウェイ**:
|
||||
|
||||
Higressはセキュリティゲートウェイとして機能し、WAF機能を提供し、key-auth、hmac-auth、jwt-auth、basic-auth、oidcなどのさまざまな認証戦略をサポートします。
|
||||
|
||||
## 主な利点
|
||||
|
||||
- **プロダクションレベル**
|
||||
|
||||
Alibabaで2年以上のプロダクション検証を経た内部製品から派生し、毎秒数十万のリクエストを処理する大規模なシナリオをサポートします。
|
||||
|
||||
Nginxのリロードによるトラフィックの揺れを完全に排除し、構成変更がミリ秒単位で反映され、ビジネスに影響を与えません。AIビジネスなどの長時間接続シナリオに特に適しています。
|
||||
|
||||
- **ストリーム処理**
|
||||
|
||||
リクエスト/レスポンスボディの完全なストリーム処理をサポートし、Wasmプラグインを使用してSSE(Server-Sent Events)などのストリームプロトコルのメッセージをカスタマイズして処理できます。
|
||||
|
||||
AIビジネスなどの大帯域幅シナリオで、メモリ使用量を大幅に削減できます。
|
||||
|
||||
- **拡張性**
|
||||
|
||||
AI、トラフィック管理、セキュリティ保護などの一般的な機能をカバーする豊富な公式プラグインライブラリを提供し、90%以上のビジネスシナリオのニーズを満たします。
|
||||
|
||||
Wasmプラグイン拡張を主力とし、サンドボックス隔離を通じてメモリの安全性を確保し、複数のプログラミング言語をサポートし、プラグインバージョンの独立したアップグレードを許可し、トラフィックに影響を与えずにゲートウェイロジックをホットアップデートできます。
|
||||
|
||||
- **安全で使いやすい**
|
||||
|
||||
Ingress APIおよびGateway API標準に基づき、すぐに使用できるUIコンソールを提供し、WAF保護プラグイン、IP/Cookie CC保護プラグインをすぐに使用できます。
|
||||
|
||||
Let's Encryptの自動証明書発行および更新をサポートし、K8sを使用せずにデプロイでき、1行のDockerコマンドで起動でき、個人開発者にとって便利です。
|
||||
|
||||
|
||||
## 機能紹介
|
||||
|
||||
### AIゲートウェイデモ展示
|
||||
|
||||
[OpenAIから他の大規模モデルへの移行を30秒で完了
|
||||
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
|
||||
|
||||
|
||||
### Higress UIコンソール
|
||||
|
||||
- **豊富な可観測性**
|
||||
|
||||
すぐに使用できる可観測性を提供し、Grafana&Prometheusは組み込みのものを使用することも、自分で構築したものを接続することもできます。
|
||||
|
||||

|
||||
|
||||
|
||||
- **プラグイン拡張メカニズム**
|
||||
|
||||
公式にはさまざまなプラグインが提供されており、ユーザーは[独自のプラグインを開発](./plugins/wasm-go)し、Docker/OCIイメージとして構築し、コンソールで構成して、プラグインロジックをリアルタイムで変更できます。トラフィックに影響を与えずにプラグインロジックをホットアップデートできます。
|
||||
|
||||

|
||||
|
||||
|
||||
- **さまざまなサービス発見**
|
||||
|
||||
デフォルトでK8s Serviceサービス発見を提供し、構成を通じてNacos/ZooKeeperなどのレジストリに接続してサービスを発見することも、静的IPまたはDNSに基づいて発見することもできます。
|
||||
|
||||

|
||||
|
||||
|
||||
- **ドメインと証明書**
|
||||
|
||||
TLS証明書を作成および管理し、ドメインのHTTP/HTTPS動作を構成できます。ドメインポリシーでは、特定のドメインに対してプラグインを適用することができます。
|
||||
|
||||

|
||||
|
||||
|
||||
- **豊富なルーティング機能**
|
||||
|
||||
上記で定義されたサービス発見メカニズムを通じて、発見されたサービスはサービスリストに表示されます。ルーティングを作成する際に、ドメインを選択し、ルーティングマッチングメカニズムを定義し、ターゲットサービスを選択してルーティングを行います。ルーティングポリシーでは、特定のルーティングに対してプラグインを適用することができます。
|
||||
|
||||

|
||||
|
||||
|
||||
## コミュニティ
|
||||
|
||||
### 感謝
|
||||
|
||||
EnvoyとIstioのオープンソースの取り組みがなければ、Higressは実現できませんでした。これらのプロジェクトに最も誠実な敬意を表します。
|
||||
|
||||
### 交流グループ
|
||||
|
||||

|
||||
|
||||
### 技術共有
|
||||
|
||||
WeChat公式アカウント:
|
||||
|
||||

|
||||
|
||||
### 関連リポジトリ
|
||||
|
||||
- Higressコンソール:https://github.com/higress-group/higress-console
|
||||
- Higress(スタンドアロン版):https://github.com/higress-group/higress-standalone
|
||||
|
||||
### 貢献者
|
||||
|
||||
<a href="https://github.com/alibaba/higress/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
|
||||
</a>
|
||||
|
||||
### スターの歴史
|
||||
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ トップに戻る ↑
|
||||
</a>
|
||||
</p>
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
| < 1.0.0 | :x: |
|
||||
|
||||
|
||||
@@ -284,6 +284,10 @@ spec:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
protocol:
|
||||
type: string
|
||||
sni:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
zkServicesPath:
|
||||
|
||||
@@ -126,6 +126,8 @@ type RegistryConfig struct {
|
||||
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
|
||||
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
|
||||
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
|
||||
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) Reset() {
|
||||
@@ -279,6 +281,20 @@ func (x *RegistryConfig) GetAuthSecretName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetProtocol() string {
|
||||
if x != nil {
|
||||
return x.Protocol
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetSni() string {
|
||||
if x != nil {
|
||||
return x.Sni
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_networking_v1_mcp_bridge_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
@@ -292,7 +308,7 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72,
|
||||
0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xa5, 0x05, 0x0a,
|
||||
0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x05, 0x0a,
|
||||
0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0,
|
||||
0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
@@ -335,10 +351,13 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e,
|
||||
0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||
0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65,
|
||||
0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e,
|
||||
0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73,
|
||||
0x6e, 0x69, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73,
|
||||
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f,
|
||||
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -64,4 +64,6 @@ message RegistryConfig {
|
||||
string consulServiceTag = 15;
|
||||
int64 consulRefreshInterval = 16;
|
||||
string authSecretName = 17;
|
||||
string protocol = 18;
|
||||
string sni = 19;
|
||||
}
|
||||
|
||||
Submodule envoy/envoy updated: 9c9c3b717c...e9302f5574
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.0
|
||||
appVersion: 2.0.3
|
||||
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: 2.0.0
|
||||
version: 2.0.3
|
||||
|
||||
@@ -284,6 +284,10 @@ spec:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
protocol:
|
||||
type: string
|
||||
sni:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
zkServicesPath:
|
||||
@@ -302,3 +306,4 @@ spec:
|
||||
subresources:
|
||||
status: {}
|
||||
|
||||
---
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
|
||||
@@ -101,3 +101,15 @@ higress: {{ include "controller.name" . }}
|
||||
true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.podMonitor.gvk" -}}
|
||||
{{- if eq .Values.gateway.metrics.provider "monitoring.coreos.com" -}}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PodMonitor
|
||||
{{- else if eq .Values.gateway.metrics.provider "operator.victoriametrics.com" -}}
|
||||
apiVersion: operator.victoriametrics.com/v1beta1
|
||||
kind: VMPodScrape
|
||||
{{- else -}}
|
||||
{{- fail "unexpected gateway.metrics.provider" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
326
helm/core/templates/_pod.tpl
Normal file
326
helm/core/templates/_pod.tpl
Normal file
@@ -0,0 +1,326 @@
|
||||
|
||||
{{/*
|
||||
Rendering the pod template of gateway component.
|
||||
*/}}
|
||||
{{- define "gateway.podTemplate" -}}
|
||||
{{- $o11y := .Values.global.o11y -}}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
"enableHigressIstio": "true"
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.podAnnotations }}
|
||||
{{- toYaml .Values.gateway.podAnnotations | nindent 6 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
sidecar.istio.io/inject: "false"
|
||||
{{- with .Values.gateway.revision }}
|
||||
istio.io/rev: {{ . }}
|
||||
{{- end }}
|
||||
{{- include "gateway.selectorLabels" . | nindent 6 }}
|
||||
spec:
|
||||
{{- with .Values.gateway.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "gateway.serviceAccountName" . }}
|
||||
{{- if .Values.global.priorityClassName }}
|
||||
priorityClassName: "{{ .Values.global.priorityClassName }}"
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- if .Values.gateway.securityContext }}
|
||||
{{- toYaml .Values.gateway.securityContext | nindent 6 }}
|
||||
{{- else if and .Values.gateway.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:
|
||||
- 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 10 }}
|
||||
{{- else if and .Values.gateway.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: 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: 15020
|
||||
protocol: TCP
|
||||
name: istio-prom
|
||||
- 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 10 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/secrets/workload-spiffe-uds
|
||||
name: workload-socket
|
||||
- mountPath: /var/run/secrets/credential-uds
|
||||
name: credential-socket
|
||||
- mountPath: /var/run/secrets/workload-spiffe-credentials
|
||||
name: workload-certs
|
||||
{{- 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: higress-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 $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
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: workload-socket
|
||||
- emptyDir: {}
|
||||
name: credential-socket
|
||||
- emptyDir: {}
|
||||
name: workload-certs
|
||||
{{- if eq (include "controller.jwtPolicy" .) "third-party-jwt" }}
|
||||
- name: istio-token
|
||||
projected:
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
audience: istio-ca
|
||||
expirationSeconds: 43200
|
||||
path: istio-token
|
||||
{{- end }}
|
||||
- name: higress-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 -}}
|
||||
@@ -116,6 +116,12 @@ data:
|
||||
{{- $existingData = index $existingConfig.data "higress" | default "{}" | fromYaml }}
|
||||
{{- end }}
|
||||
{{- $newData := dict }}
|
||||
{{- if hasKey .Values "upstream" }}
|
||||
{{- $_ := set $newData "upstream" .Values.upstream }}
|
||||
{{- end }}
|
||||
{{- if hasKey .Values "downstream" }}
|
||||
{{- $_ := set $newData "downstream" .Values.downstream }}
|
||||
{{- end }}
|
||||
{{- if and (hasKey .Values "tracing") .Values.tracing.enable }}
|
||||
{{- $_ := set $newData "tracing" .Values.tracing }}
|
||||
{{- end }}
|
||||
@@ -155,44 +161,12 @@ data:
|
||||
"transport_api_version": "V3",
|
||||
"grpc_service": {
|
||||
"envoy_grpc": {
|
||||
"cluster_name": "service_skywalking"
|
||||
"cluster_name": "outbound|{{ .Values.tracing.skywalking.port }}||{{ .Values.tracing.skywalking.service }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"static_resources": {
|
||||
"clusters": [
|
||||
{
|
||||
"name": "service_skywalking",
|
||||
"type": "LOGICAL_DNS",
|
||||
"connect_timeout": "5s",
|
||||
"http2_protocol_options": {
|
||||
},
|
||||
"dns_lookup_family": "V4_ONLY",
|
||||
"lb_policy": "ROUND_ROBIN",
|
||||
"load_assignment": {
|
||||
"cluster_name": "service_skywalking",
|
||||
"endpoints": [
|
||||
{
|
||||
"lb_endpoints": [
|
||||
{
|
||||
"endpoint": {
|
||||
"address": {
|
||||
"socket_address": {
|
||||
"address": "{{ .Values.tracing.skywalking.service }}",
|
||||
"port_value": "{{ .Values.tracing.skywalking.port }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
---
|
||||
{{- end }}
|
||||
|
||||
@@ -129,3 +129,10 @@ rules:
|
||||
- apiGroups: ["networking.internal.knative.dev"]
|
||||
resources: ["ingresses/status"]
|
||||
verbs: ["get","patch","update"]
|
||||
# gateway api need
|
||||
- apiGroups: ["apps"]
|
||||
verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ]
|
||||
resources: [ "deployments" ]
|
||||
- apiGroups: [""]
|
||||
verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ]
|
||||
resources: [ "serviceaccounts"]
|
||||
|
||||
@@ -69,6 +69,12 @@ spec:
|
||||
fieldPath: spec.serviceAccountName
|
||||
- name: DOMAIN_SUFFIX
|
||||
value: {{ .Values.global.proxy.clusterDomain }}
|
||||
- name: GATEWAY_NAME
|
||||
value: {{ include "gateway.name" . }}
|
||||
- name: PILOT_ENABLE_GATEWAY_API
|
||||
value: "{{ .Values.global.enableGatewayAPI }}"
|
||||
- name: PILOT_ENABLE_ALPHA_GATEWAY_API
|
||||
value: "{{ .Values.global.enableGatewayAPI }}"
|
||||
{{- if .Values.controller.env }}
|
||||
{{- range $key, $val := .Values.controller.env }}
|
||||
- name: {{ $key }}
|
||||
@@ -215,14 +221,14 @@ spec:
|
||||
- name: HIGRESS_ENABLE_ISTIO_API
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if .Values.global.enableGatewayAPI }}
|
||||
- name: PILOT_ENABLE_GATEWAY_API
|
||||
value: "true"
|
||||
value: "false"
|
||||
- name: PILOT_ENABLE_ALPHA_GATEWAY_API
|
||||
value: "false"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_STATUS
|
||||
value: "true"
|
||||
value: "false"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER
|
||||
value: "false"
|
||||
{{- end }}
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: CUSTOM_CA_CERT_NAME
|
||||
value: "higress-ca-root-cert"
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
{{- if $kernelVersion }}
|
||||
{{- $kernelVersion = regexFind "^(\\d+\\.\\d+\\.\\d+)" $kernelVersion }}
|
||||
{{- if and $kernelVersion (semverCompare "<4.11.0" $kernelVersion) }}
|
||||
{{- $unprivilegedPortSupported = false }}
|
||||
{{- $unprivilegedPortSupported = false }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- $_ := set .Values.gateway "unprivilegedPortSupported" $unprivilegedPortSupported -}}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
@@ -23,310 +25,5 @@ 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 }}
|
||||
{{- include "gateway.podTemplate" $ | nindent 2 -}}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{{- if eq .Values.gateway.kind "Deployment" -}}
|
||||
{{- $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 }}
|
||||
{{- $unprivilegedPortSupported = false }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- $_ := set .Values.gateway "unprivilegedPortSupported" $unprivilegedPortSupported -}}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -38,311 +39,7 @@ spec:
|
||||
{{- else }}
|
||||
maxUnavailable: {{ .Values.gateway.rollingMaxUnavailable }}
|
||||
{{- end }}
|
||||
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: 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: 15020
|
||||
protocol: TCP
|
||||
name: istio-prom
|
||||
- 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 }}
|
||||
|
||||
{{- include "gateway.podTemplate" $ | nindent 2 -}}
|
||||
|
||||
{{- end }}
|
||||
|
||||
22
helm/core/templates/fallback-envoyfilter.yaml
Normal file
22
helm/core/templates/fallback-envoyfilter.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: EnvoyFilter
|
||||
metadata:
|
||||
name: {{ include "gateway.name" . }}-global-custom-response
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4}}
|
||||
spec:
|
||||
configPatches:
|
||||
- applyTo: HTTP_FILTER
|
||||
match:
|
||||
context: GATEWAY
|
||||
listener:
|
||||
filterChain:
|
||||
filter:
|
||||
name: envoy.filters.network.http_connection_manager
|
||||
patch:
|
||||
operation: INSERT_FIRST
|
||||
value:
|
||||
name: envoy.filters.http.custom_response
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.custom_response.v3.CustomResponse
|
||||
@@ -1,6 +1,8 @@
|
||||
{{- if .Values.global.ingressClass }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: {{ .Values.global.ingressClass }}
|
||||
spec:
|
||||
controller: higress.io/higress-controller
|
||||
controller: higress.io/higress-controller
|
||||
{{- end }}
|
||||
|
||||
45
helm/core/templates/podmonitor.yaml
Normal file
45
helm/core/templates/podmonitor.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
{{- if .Values.gateway.metrics.enabled }}
|
||||
{{- include "gateway.podMonitor.gvk" . }}
|
||||
metadata:
|
||||
name: {{ printf "%s-metrics" (include "gateway.name" .) | trunc 63 | trimSuffix "-" }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4}}
|
||||
annotations:
|
||||
{{- .Values.gateway.annotations | toYaml | nindent 4 }}
|
||||
spec:
|
||||
jobLabel: "app.kubernetes.io/name"
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gateway.selectorLabels" . | nindent 6 }}
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- {{ .Release.Namespace }}
|
||||
podMetricsEndpoints:
|
||||
- port: istio-prom
|
||||
path: /stats/prometheus
|
||||
{{- if .Values.gateway.metrics.interval }}
|
||||
interval: {{ .Values.gateway.metrics.interval }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.metrics.scrapeTimeout }}
|
||||
scrapeTimeout: {{ .Values.gateway.metrics.scrapeTimeout }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.metrics.honorLabels }}
|
||||
honorLabels: {{ .Values.gateway.metrics.honorLabels }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.metrics.metricRelabelings }}
|
||||
metricRelabelings: {{ toYaml .Values.gateway.metrics.metricRelabelings | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.metrics.relabelings }}
|
||||
relabelings: {{ toYaml .Values.gateway.metrics.relabelings | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.metrics.metricRelabelConfigs }}
|
||||
metricRelabelings: {{ toYaml .Values.gateway.metrics.metricRelabelConfigs | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.metrics.relabelConfigs }}
|
||||
relabelings: {{ toYaml .Values.gateway.metrics.relabelConfigs | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if $.Values.gateway.metrics.rawSpec }}
|
||||
{{- $.Values.gateway.metrics.rawSpec | toYaml | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -26,7 +26,7 @@ global:
|
||||
autoscalingv2API: true
|
||||
local: false # When deploying to a local cluster (e.g.: kind cluster), set this to true.
|
||||
kind: false # Deprecated. Please use "global.local" instead. Will be removed later.
|
||||
enableIstioAPI: false
|
||||
enableIstioAPI: true
|
||||
enableGatewayAPI: false
|
||||
# Deprecated
|
||||
enableHigressIstio: false
|
||||
@@ -136,7 +136,6 @@ global:
|
||||
excludeInboundPorts: ""
|
||||
includeInboundPorts: "*"
|
||||
|
||||
|
||||
# istio egress capture allowlist
|
||||
# https://istio.io/docs/tasks/traffic-management/egress.html#calling-external-services-directly
|
||||
# example: includeIPRanges: "172.30.0.0/16,172.20.0.0/16"
|
||||
@@ -322,8 +321,8 @@ global:
|
||||
# Host:Port for submitting traces to the Datadog agent.
|
||||
address: "$(HOST_IP):8126"
|
||||
lightstep:
|
||||
address: "" # example: lightstep-satellite:443
|
||||
accessToken: "" # example: abcdefg1234567
|
||||
address: "" # example: lightstep-satellite:443
|
||||
accessToken: "" # example: abcdefg1234567
|
||||
stackdriver:
|
||||
# enables trace output to stdout.
|
||||
debug: false
|
||||
@@ -449,25 +448,25 @@ gateway:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/path: "/stats/prometheus"
|
||||
sidecar.istio.io/inject: "false"
|
||||
|
||||
|
||||
# Define the security context for the pod.
|
||||
# If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443.
|
||||
# On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl.
|
||||
securityContext: ~
|
||||
containerSecurityContext: ~
|
||||
|
||||
|
||||
service:
|
||||
# Type of service. Set to "None" to disable the service entirely
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: http2
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
- name: https
|
||||
port: 443
|
||||
protocol: TCP
|
||||
targetPort: 443
|
||||
- name: http2
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
- name: https
|
||||
port: 443
|
||||
protocol: TCP
|
||||
targetPort: 443
|
||||
annotations: {}
|
||||
loadBalancerIP: ""
|
||||
loadBalancerClass: ""
|
||||
@@ -476,7 +475,7 @@ gateway:
|
||||
|
||||
rollingMaxSurge: 100%
|
||||
rollingMaxUnavailable: 25%
|
||||
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 2000m
|
||||
@@ -484,22 +483,39 @@ gateway:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2048Mi
|
||||
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
|
||||
tolerations: []
|
||||
|
||||
|
||||
affinity: {}
|
||||
|
||||
|
||||
# If specified, the gateway will act as a network gateway for the given network.
|
||||
networkGateway: ""
|
||||
|
||||
|
||||
metrics:
|
||||
# If true, create PodMonitor or VMPodScrape for gateway
|
||||
enabled: false
|
||||
# provider group name for CustomResourceDefinition, can be monitoring.coreos.com or operator.victoriametrics.com
|
||||
provider: monitoring.coreos.com
|
||||
interval: ""
|
||||
scrapeTimeout: ""
|
||||
honorLabels: false
|
||||
# for monitoring.coreos.com/v1.PodMonitor
|
||||
metricRelabelings: []
|
||||
relabelings: []
|
||||
# for operator.victoriametrics.com/v1beta1.VMPodScrape
|
||||
metricRelabelConfigs: []
|
||||
relabelConfigs: []
|
||||
# some more raw podMetricsEndpoints spec
|
||||
rawSpec: {}
|
||||
|
||||
controller:
|
||||
name: "higress-controller"
|
||||
replicas: 1
|
||||
@@ -510,22 +526,20 @@ controller:
|
||||
env: {}
|
||||
|
||||
labels: {}
|
||||
|
||||
probe: {
|
||||
httpGet: {
|
||||
path: /ready,
|
||||
port: 8888,
|
||||
},
|
||||
initialDelaySeconds: 1,
|
||||
periodSeconds: 3,
|
||||
timeoutSeconds: 5
|
||||
}
|
||||
|
||||
|
||||
probe:
|
||||
{
|
||||
httpGet: { path: /ready, port: 8888 },
|
||||
initialDelaySeconds: 1,
|
||||
periodSeconds: 3,
|
||||
timeoutSeconds: 5,
|
||||
}
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
rbac:
|
||||
create: true
|
||||
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
@@ -534,37 +548,30 @@ controller:
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
|
||||
podSecurityContext:
|
||||
{}
|
||||
# fsGroup: 2000
|
||||
|
||||
ports: [
|
||||
{
|
||||
"name": "http",
|
||||
"protocol": "TCP",
|
||||
"port": 8888,
|
||||
"targetPort": 8888,
|
||||
},
|
||||
{
|
||||
"name": "http-solver",
|
||||
"protocol": "TCP",
|
||||
"port": 8889,
|
||||
"targetPort": 8889,
|
||||
},
|
||||
{
|
||||
"name": "grpc",
|
||||
"protocol": "TCP",
|
||||
"port": 15051,
|
||||
"targetPort": 15051,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
ports:
|
||||
[
|
||||
{ "name": "http", "protocol": "TCP", "port": 8888, "targetPort": 8888 },
|
||||
{
|
||||
"name": "http-solver",
|
||||
"protocol": "TCP",
|
||||
"port": 8889,
|
||||
"targetPort": 8889,
|
||||
},
|
||||
{ "name": "grpc", "protocol": "TCP", "port": 15051, "targetPort": 15051 },
|
||||
]
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
securityContext: {}
|
||||
|
||||
securityContext:
|
||||
{}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
@@ -579,11 +586,11 @@ controller:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2048Mi
|
||||
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
|
||||
tolerations: []
|
||||
|
||||
|
||||
affinity: {}
|
||||
|
||||
autoscaling:
|
||||
@@ -594,7 +601,7 @@ controller:
|
||||
automaticHttps:
|
||||
enabled: true
|
||||
email: ""
|
||||
|
||||
|
||||
## Discovery Settings
|
||||
pilot:
|
||||
autoscaleEnabled: false
|
||||
@@ -656,7 +663,6 @@ pilot:
|
||||
# Additional labels to apply to the deployment.
|
||||
deploymentLabels: {}
|
||||
|
||||
|
||||
## Mesh config settings
|
||||
|
||||
# Install the mesh config map, generated from values.yaml.
|
||||
@@ -666,16 +672,31 @@ pilot:
|
||||
# Additional labels to apply on the pod level for monitoring and logging configuration.
|
||||
podLabels: {}
|
||||
|
||||
|
||||
# Tracing config settings
|
||||
tracing:
|
||||
enable: false
|
||||
sampling: 100
|
||||
timeout: 500
|
||||
skywalking:
|
||||
# access_token: ""
|
||||
service: ""
|
||||
port: 11800
|
||||
# access_token: ""
|
||||
service: ""
|
||||
port: 11800
|
||||
# zipkin:
|
||||
# service: ""
|
||||
# port: 9411
|
||||
# service: ""
|
||||
# port: 9411
|
||||
|
||||
# Downstream config settings
|
||||
downstream:
|
||||
idleTimeout: 180
|
||||
maxRequestHeadersKb: 60
|
||||
connectionBufferLimits: 32768
|
||||
http2:
|
||||
maxConcurrentStreams: 100
|
||||
initialStreamWindowSize: 65535
|
||||
initialConnectionWindowSize: 1048576
|
||||
routeTimeout: 0
|
||||
|
||||
# Upstream config settings
|
||||
upstream:
|
||||
idleTimeout: 10
|
||||
connectionBufferLimits: 10485760
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.0.0
|
||||
version: 2.0.3
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.4.3
|
||||
digest: sha256:ebfedb7faee4973b6e1e3624a9fcc20790943aef76ec60921e0010d1e62ff92a
|
||||
generated: "2024-09-13T10:36:29.963179+08:00"
|
||||
version: 1.4.5
|
||||
digest: sha256:74b772113264168483961f5d0424459fd7359adc509a4b50400229581d7cddbf
|
||||
generated: "2024-11-08T14:06:51.871719+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.0
|
||||
appVersion: 2.0.3
|
||||
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: 2.0.0
|
||||
version: 2.0.3
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.4.3
|
||||
version: 1.4.5
|
||||
type: application
|
||||
version: 2.0.0
|
||||
version: 2.0.3
|
||||
|
||||
@@ -114,6 +114,8 @@ static_resources:
|
||||
value: |
|
||||
{{ .JSONExample }}
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
connect_timeout: 30s
|
||||
|
||||
Submodule istio/istio updated: 8918eb802a...ce6a5d5934
@@ -21,7 +21,10 @@ type Protocol string
|
||||
const (
|
||||
TCP Protocol = "TCP"
|
||||
HTTP Protocol = "HTTP"
|
||||
HTTP2 Protocol = "HTTP2"
|
||||
HTTPS Protocol = "HTTPS"
|
||||
GRPC Protocol = "GRPC"
|
||||
GRPCS Protocol = "GRPCS"
|
||||
Dubbo Protocol = "Dubbo"
|
||||
Unsupported Protocol = "UnsupportedProtocol"
|
||||
)
|
||||
@@ -32,8 +35,14 @@ func ParseProtocol(s string) Protocol {
|
||||
return TCP
|
||||
case "http":
|
||||
return HTTP
|
||||
case "https":
|
||||
return HTTPS
|
||||
case "http2":
|
||||
return HTTP2
|
||||
case "grpc", "triple", "tri":
|
||||
return GRPC
|
||||
case "grpcs":
|
||||
return GRPCS
|
||||
case "dubbo":
|
||||
return Dubbo
|
||||
}
|
||||
@@ -51,7 +60,7 @@ func (p Protocol) IsTCP() bool {
|
||||
|
||||
func (p Protocol) IsHTTP() bool {
|
||||
switch p {
|
||||
case HTTP, GRPC:
|
||||
case HTTP, GRPC, GRPCS, HTTP2, HTTPS:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -60,7 +69,16 @@ func (p Protocol) IsHTTP() bool {
|
||||
|
||||
func (p Protocol) IsGRPC() bool {
|
||||
switch p {
|
||||
case GRPC:
|
||||
case GRPC, GRPCS:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (i Protocol) IsHTTPS() bool {
|
||||
switch i {
|
||||
case HTTPS, GRPCS:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -23,3 +23,7 @@ const KnativeIngressCRDName = "ingresses.networking.internal.knative.dev"
|
||||
const KnativeServicesCRDName = "services.serving.knative.dev"
|
||||
|
||||
const ManagedGatewayController = "higress.io/gateway-controller"
|
||||
|
||||
const RegistryTypeLabelKey = "higress-registry-type"
|
||||
|
||||
const RegistryNameLabelKey = "higress-registry-name"
|
||||
|
||||
@@ -19,6 +19,7 @@ import "istio.io/pkg/env"
|
||||
var (
|
||||
PodNamespace = env.RegisterStringVar("POD_NAMESPACE", "higress-system", "").Get()
|
||||
PodName = env.RegisterStringVar("POD_NAME", "", "").Get()
|
||||
GatewayName = env.RegisterStringVar("GATEWAY_NAME", "higress-gateway", "").Get()
|
||||
// Revision is the value of the Istio control plane revision, e.g. "canary",
|
||||
// and is the value used by the "istio.io/rev" label.
|
||||
Revision = env.Register("REVISION", "", "").Get()
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
extensions "istio.io/api/extensions/v1alpha1"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
istiotype "istio.io/api/type/v1beta1"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
istiomodel "istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/util/protoconv"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
@@ -53,6 +54,7 @@ import (
|
||||
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"
|
||||
higressconst "github.com/alibaba/higress/pkg/config/constants"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/configmap"
|
||||
@@ -234,8 +236,9 @@ func (m *IngressConfig) AddLocalCluster(options common.Options) {
|
||||
ingressController = ingressv1.NewController(m.localKubeClient, m.localKubeClient, options, secretController)
|
||||
}
|
||||
m.remoteIngressControllers[options.ClusterId] = ingressController
|
||||
|
||||
m.remoteGatewayControllers[options.ClusterId] = gateway.NewController(m.localKubeClient, options)
|
||||
if features.EnableGatewayAPI {
|
||||
m.remoteGatewayControllers[options.ClusterId] = gateway.NewController(m.localKubeClient, options)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressConfig) List(typ config.GroupVersionKind, namespace string) []config.Config {
|
||||
@@ -628,8 +631,8 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
|
||||
if m.RegistryReconciler == nil {
|
||||
return nil
|
||||
}
|
||||
serviceEntries := m.RegistryReconciler.GetAllServiceEntryWrapper()
|
||||
IngressLog.Infof("Found http2rpc serviceEntries %s", serviceEntries)
|
||||
serviceEntries := m.RegistryReconciler.GetAllServiceWrapper()
|
||||
IngressLog.Infof("Found mcp serviceEntries %v", serviceEntries)
|
||||
out := make([]config.Config, 0, len(serviceEntries))
|
||||
for _, se := range serviceEntries {
|
||||
out = append(out, config.Config{
|
||||
@@ -638,6 +641,10 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
|
||||
Name: se.ServiceEntry.Hosts[0],
|
||||
Namespace: "mcp",
|
||||
CreationTimestamp: se.GetCreateTime(),
|
||||
Labels: map[string]string{
|
||||
higressconst.RegistryTypeLabelKey: se.RegistryType,
|
||||
higressconst.RegistryNameLabelKey: se.RegistryName,
|
||||
},
|
||||
},
|
||||
Spec: se.ServiceEntry,
|
||||
})
|
||||
@@ -703,6 +710,32 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
|
||||
destinationRules[serviceName] = dr
|
||||
}
|
||||
|
||||
if m.RegistryReconciler != nil {
|
||||
drws := m.RegistryReconciler.GetAllDestinationRuleWrapper()
|
||||
IngressLog.Infof("Found mcp destinationRules: %v", drws)
|
||||
for _, destinationRuleWrapper := range drws {
|
||||
serviceName := destinationRuleWrapper.ServiceKey.ServiceFQDN
|
||||
dr, exist := destinationRules[serviceName]
|
||||
if !exist {
|
||||
destinationRules[serviceName] = destinationRuleWrapper
|
||||
} else if dr.DestinationRule.TrafficPolicy != nil {
|
||||
portTrafficPolicy := destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings[0]
|
||||
portUpdated := false
|
||||
for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
|
||||
if policy.Port.Number == portTrafficPolicy.Port.Number {
|
||||
policy.Tls = portTrafficPolicy.Tls
|
||||
portUpdated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if portUpdated {
|
||||
continue
|
||||
}
|
||||
dr.DestinationRule.TrafficPolicy.PortLevelSettings = append(dr.DestinationRule.TrafficPolicy.PortLevelSettings, portTrafficPolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]config.Config, 0, len(destinationRules))
|
||||
for _, dr := range destinationRules {
|
||||
sort.SliceStable(dr.DestinationRule.TrafficPolicy.PortLevelSettings, func(i, j int) bool {
|
||||
@@ -727,6 +760,7 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
|
||||
Spec: dr.DestinationRule,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1034,16 +1068,27 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
}
|
||||
if m.RegistryReconciler == nil {
|
||||
m.RegistryReconciler = reconcile.NewReconciler(func() {
|
||||
metadata := config.Meta{
|
||||
seMetadata := config.Meta{
|
||||
Name: "mcpbridge-serviceentry",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.ServiceEntry,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
drMetadata := config.Meta{
|
||||
Name: "mcpbridge-destinationrule",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.DestinationRule,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
for _, f := range m.serviceEntryHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd serviceEntry update")
|
||||
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, istiomodel.EventUpdate)
|
||||
f(config.Config{Meta: seMetadata}, config.Config{Meta: seMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
for _, f := range m.destinationRuleHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd destinationRule update")
|
||||
f(config.Config{Meta: drMetadata}, config.Config{Meta: drMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace)
|
||||
}
|
||||
@@ -1489,7 +1534,7 @@ func constructBasicAuthEnvoyFilter(rules *common.BasicAuthRules, namespace strin
|
||||
}, nil
|
||||
}
|
||||
|
||||
func QueryByName(serviceEntries []*memory.ServiceEntryWrapper, serviceName string) (*memory.ServiceEntryWrapper, error) {
|
||||
func QueryByName(serviceEntries []*memory.ServiceWrapper, serviceName string) (*memory.ServiceWrapper, error) {
|
||||
IngressLog.Infof("Found http2rpc serviceEntries %s", serviceEntries)
|
||||
for _, se := range serviceEntries {
|
||||
if se.ServiceName == serviceName {
|
||||
@@ -1499,7 +1544,7 @@ func QueryByName(serviceEntries []*memory.ServiceEntryWrapper, serviceName strin
|
||||
return nil, fmt.Errorf("can't find ServiceEntry by serviceName:%v", serviceName)
|
||||
}
|
||||
|
||||
func QueryRpcServiceVersion(serviceEntry *memory.ServiceEntryWrapper, serviceName string) (string, error) {
|
||||
func QueryRpcServiceVersion(serviceEntry *memory.ServiceWrapper, serviceName string) (string, error) {
|
||||
IngressLog.Infof("Found http2rpc serviceEntry %s", serviceEntry)
|
||||
IngressLog.Infof("Found http2rpc ServiceEntry %s", serviceEntry.ServiceEntry)
|
||||
IngressLog.Infof("Found http2rpc WorkloadSelector %s", serviceEntry.ServiceEntry.WorkloadSelector)
|
||||
|
||||
@@ -69,6 +69,8 @@ type Ingress struct {
|
||||
|
||||
Auth *AuthConfig
|
||||
|
||||
Mirror *MirrorConfig
|
||||
|
||||
Destination *DestinationConfig
|
||||
|
||||
IgnoreCase *IgnoreCaseConfig
|
||||
@@ -161,6 +163,7 @@ func NewAnnotationHandlerManager() AnnotationHandler {
|
||||
localRateLimit{},
|
||||
fallback{},
|
||||
auth{},
|
||||
mirror{},
|
||||
destination{},
|
||||
ignoreCaseMatching{},
|
||||
match{},
|
||||
@@ -182,6 +185,7 @@ func NewAnnotationHandlerManager() AnnotationHandler {
|
||||
retry{},
|
||||
localRateLimit{},
|
||||
fallback{},
|
||||
mirror{},
|
||||
ignoreCaseMatching{},
|
||||
match{},
|
||||
headerControl{},
|
||||
|
||||
118
pkg/ingress/kube/annotations/mirror.go
Normal file
118
pkg/ingress/kube/annotations/mirror.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2023 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
mirrorTargetService = "mirror-target-service"
|
||||
mirrorPercentage = "mirror-percentage"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &mirror{}
|
||||
_ RouteHandler = &mirror{}
|
||||
)
|
||||
|
||||
type MirrorConfig struct {
|
||||
util.ServiceInfo
|
||||
Percentage *wrappers.DoubleValue
|
||||
}
|
||||
|
||||
type mirror struct{}
|
||||
|
||||
func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if !needMirror(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
target, err := annotations.ParseStringASAP(mirrorTargetService)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Get mirror target service fail, err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
serviceInfo, err := util.ParseServiceInfo(target, config.Namespace)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Get mirror target service fail, err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
serviceLister, exist := globalContext.ClusterServiceList[config.ClusterId]
|
||||
if !exist {
|
||||
IngressLog.Errorf("service lister of cluster %s doesn't exist", config.ClusterId)
|
||||
return nil
|
||||
}
|
||||
|
||||
service, err := serviceLister.Services(serviceInfo.Namespace).Get(serviceInfo.Name)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Mirror service %s/%s within ingress %s/%s is not found, with err: %v",
|
||||
serviceInfo.Namespace, serviceInfo.Name, config.Namespace, config.Name, err)
|
||||
return nil
|
||||
}
|
||||
if service == nil {
|
||||
IngressLog.Errorf("service %s/%s within ingress %s/%s is empty value",
|
||||
serviceInfo.Namespace, serviceInfo.Name, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
if serviceInfo.Port == 0 {
|
||||
// Use the first port
|
||||
serviceInfo.Port = uint32(service.Spec.Ports[0].Port)
|
||||
}
|
||||
|
||||
var percentage *wrappers.DoubleValue
|
||||
if value, err := annotations.ParseIntASAP(mirrorPercentage); err == nil {
|
||||
if value < 100 {
|
||||
percentage = &wrappers.DoubleValue{
|
||||
Value: float64(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
ServiceInfo: serviceInfo,
|
||||
Percentage: percentage,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
if config.Mirror == nil {
|
||||
return
|
||||
}
|
||||
|
||||
route.Mirror = &networking.Destination{
|
||||
Host: util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name),
|
||||
Port: &networking.PortSelector{
|
||||
Number: config.Mirror.Port,
|
||||
},
|
||||
}
|
||||
|
||||
if config.Mirror.Percentage != nil {
|
||||
route.MirrorPercentage = &networking.Percent{
|
||||
Value: config.Mirror.Percentage.GetValue(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func needMirror(annotations Annotations) bool {
|
||||
return annotations.HasASAP(mirrorTargetService)
|
||||
}
|
||||
163
pkg/ingress/kube/annotations/mirror_test.go
Normal file
163
pkg/ingress/kube/annotations/mirror_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/golang/protobuf/proto"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseMirror(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input []map[string]string
|
||||
expect *MirrorConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetService): "test/app"},
|
||||
{buildNginxAnnotationKey(mirrorTargetService): "test/app"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetService): "test/app:8080"},
|
||||
{buildNginxAnnotationKey(mirrorTargetService): "test/app:8080"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetService): "test/app:hi"},
|
||||
{buildNginxAnnotationKey(mirrorTargetService): "test/app:hi"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetService): "test/app"},
|
||||
{buildNginxAnnotationKey(mirrorTargetService): "test/app"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mirror := mirror{}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "test",
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
}
|
||||
globalContext, cancel := initGlobalContextForService()
|
||||
defer cancel()
|
||||
|
||||
for _, in := range testCase.input {
|
||||
_ = mirror.Parse(in, config, globalContext)
|
||||
if !reflect.DeepEqual(testCase.expect, config.Mirror) {
|
||||
t.Log("expect:", *testCase.expect)
|
||||
t.Log("actual:", *config.Mirror)
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirror_ApplyRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Mirror: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "default",
|
||||
Name: "test",
|
||||
},
|
||||
Port: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Mirror: &networking.Destination{
|
||||
Host: "test.default.svc.cluster.local",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mirror := mirror{}
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
mirror.ApplyRoute(testCase.input, testCase.config)
|
||||
if !proto.Equal(testCase.input, testCase.expect) {
|
||||
t.Fatal("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,15 @@ type WrapperGateway struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func CreateMcpServiceKey(host string, portNumber int32) ServiceKey {
|
||||
return ServiceKey{
|
||||
Namespace: "mcp",
|
||||
Name: host,
|
||||
ServiceFQDN: host,
|
||||
Port: portNumber,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WrapperGateway) IsHTTPS() bool {
|
||||
if w.Gateway == nil || len(w.Gateway.Servers) == 0 {
|
||||
return false
|
||||
|
||||
@@ -255,6 +255,59 @@ func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
configPatches := []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: util.BuildPatchStruct(tracingConfig),
|
||||
},
|
||||
},
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.router",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: util.BuildPatchStruct(`{
|
||||
"name":"envoy.filters.http.router",
|
||||
"typed_config":{
|
||||
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
|
||||
"start_child_span": true
|
||||
}
|
||||
}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
patches := t.constructTracingExtendPatches(tracing)
|
||||
configPatches = append(configPatches, patches...)
|
||||
|
||||
config := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
@@ -262,55 +315,7 @@ func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: util.BuildPatchStruct(tracingConfig),
|
||||
},
|
||||
},
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.router",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: util.BuildPatchStruct(`{
|
||||
"name":"envoy.filters.http.router",
|
||||
"typed_config":{
|
||||
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
|
||||
"start_child_span": true
|
||||
}
|
||||
}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
ConfigPatches: configPatches,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -318,6 +323,52 @@ func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func tracingClusterName(port, service string) string {
|
||||
return fmt.Sprintf("outbound|%s||%s", port, service)
|
||||
}
|
||||
|
||||
func (t *TracingController) constructHTTP2ProtocolOptionsPatch(port, service string) *networking.EnvoyFilter_EnvoyConfigObjectPatch {
|
||||
http2ProtocolOptions := `{"typed_extension_protocol_options": {
|
||||
"envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
|
||||
"@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
|
||||
"explicit_http_config": {
|
||||
"http2_protocol_options": {}
|
||||
}
|
||||
}
|
||||
}}`
|
||||
|
||||
return &networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
ApplyTo: networking.EnvoyFilter_CLUSTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
|
||||
Cluster: &networking.EnvoyFilter_ClusterMatch{
|
||||
Name: tracingClusterName(port, service),
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: util.BuildPatchStruct(http2ProtocolOptions),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TracingController) constructTracingExtendPatches(tracing *Tracing) []*networking.EnvoyFilter_EnvoyConfigObjectPatch {
|
||||
if tracing == nil {
|
||||
return nil
|
||||
}
|
||||
var patches []*networking.EnvoyFilter_EnvoyConfigObjectPatch
|
||||
if skywalking := tracing.Skywalking; skywalking != nil {
|
||||
patches = append(patches, t.constructHTTP2ProtocolOptionsPatch(skywalking.Port, skywalking.Service))
|
||||
}
|
||||
if otel := tracing.OpenTelemetry; otel != nil {
|
||||
patches = append(patches, t.constructHTTP2ProtocolOptionsPatch(otel.Port, otel.Service))
|
||||
}
|
||||
|
||||
return patches
|
||||
}
|
||||
|
||||
func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace string) string {
|
||||
tracingConfig := ""
|
||||
timeout := float32(tracing.Timeout) / 1000
|
||||
@@ -338,7 +389,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
|
||||
},
|
||||
"grpc_service": {
|
||||
"envoy_grpc": {
|
||||
"cluster_name": "outbound|%s||%s"
|
||||
"cluster_name": "%s"
|
||||
},
|
||||
"timeout": "%.3fs"
|
||||
}
|
||||
@@ -349,7 +400,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, namespace, skywalking.AccessToken, skywalking.Port, skywalking.Service, timeout, tracing.Sampling)
|
||||
}`, namespace, skywalking.AccessToken, tracingClusterName(skywalking.Port, skywalking.Service), timeout, tracing.Sampling)
|
||||
}
|
||||
|
||||
if tracing.Zipkin != nil {
|
||||
@@ -363,7 +414,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
|
||||
"name": "envoy.tracers.zipkin",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/envoy.config.trace.v3.ZipkinConfig",
|
||||
"collector_cluster": "outbound|%s||%s",
|
||||
"collector_cluster": "%s",
|
||||
"collector_endpoint": "/api/v2/spans",
|
||||
"collector_hostname": "higress-gateway",
|
||||
"collector_endpoint_version": "HTTP_JSON",
|
||||
@@ -375,7 +426,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, zipkin.Port, zipkin.Service, tracing.Sampling)
|
||||
}`, tracingClusterName(zipkin.Port, zipkin.Service), tracing.Sampling)
|
||||
}
|
||||
|
||||
if tracing.OpenTelemetry != nil {
|
||||
@@ -392,7 +443,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
|
||||
"service_name": "higress-gateway.%s",
|
||||
"grpc_service": {
|
||||
"envoy_grpc": {
|
||||
"cluster_name": "outbound|%s||%s"
|
||||
"cluster_name": "%s"
|
||||
},
|
||||
"timeout": "%.3fs"
|
||||
}
|
||||
@@ -403,7 +454,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, namespace, opentelemetry.Port, opentelemetry.Service, timeout, tracing.Sampling)
|
||||
}`, namespace, tracingClusterName(opentelemetry.Port, opentelemetry.Service), timeout, tracing.Sampling)
|
||||
}
|
||||
return tracingConfig
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
kubecredentials "istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
kubecontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/status"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
@@ -48,6 +49,7 @@ type gatewayController struct {
|
||||
store model.ConfigStoreController
|
||||
credsController credentials.MulticlusterController
|
||||
istioController *istiogateway.Controller
|
||||
statusManager *status.Manager
|
||||
|
||||
resourceUpToDate atomic.Bool
|
||||
}
|
||||
@@ -76,9 +78,10 @@ func NewController(client kube.Client, options common.Options) common.GatewayCon
|
||||
istioController.DefaultGatewaySelector = map[string]string{options.GatewaySelectorKey: options.GatewaySelectorValue}
|
||||
}
|
||||
|
||||
var statusManager *status.Manager = nil
|
||||
if options.EnableStatus {
|
||||
// TODO: Add status sync support
|
||||
//istioController.SetStatusWrite(true,)
|
||||
statusManager = status.NewManager(store)
|
||||
istioController.SetStatusWrite(true, statusManager)
|
||||
} else {
|
||||
IngressLog.Infof("Disable status update for cluster %s", clusterId)
|
||||
}
|
||||
@@ -87,6 +90,7 @@ func NewController(client kube.Client, options common.Options) common.GatewayCon
|
||||
store: store,
|
||||
credsController: credsController,
|
||||
istioController: istioController,
|
||||
statusManager: statusManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +152,9 @@ func (g *gatewayController) Run(stop <-chan struct{}) {
|
||||
})
|
||||
go g.store.Run(stop)
|
||||
go g.istioController.Run(stop)
|
||||
if g.statusManager != nil {
|
||||
g.statusManager.Start(stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gatewayController) SetWatchErrorHandler(f func(r *cache.Reflector, err error)) error {
|
||||
|
||||
@@ -15,26 +15,37 @@
|
||||
package istio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
serviceRegistryKube "istio.io/istio/pilot/pkg/serviceregistry/kube"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config/host"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GatewayContext contains a minimal subset of push context functionality to be exposed to GatewayAPIControllers
|
||||
type GatewayContext struct {
|
||||
ps *model.PushContext
|
||||
// Start - Updated by Higress
|
||||
client kube.Client
|
||||
domainSuffix string
|
||||
clusterID cluster.ID
|
||||
// End - Updated by Higress
|
||||
}
|
||||
|
||||
func NewGatewayContext(ps *model.PushContext) GatewayContext {
|
||||
return GatewayContext{ps}
|
||||
// Start - Updated by Higress
|
||||
|
||||
func NewGatewayContext(ps *model.PushContext, client kube.Client, domainSuffix string, clusterID cluster.ID) GatewayContext {
|
||||
return GatewayContext{ps, client, domainSuffix, clusterID}
|
||||
}
|
||||
|
||||
// ResolveGatewayInstances attempts to resolve all instances that a gateway will be exposed on.
|
||||
@@ -59,26 +70,20 @@ func (gc GatewayContext) ResolveGatewayInstances(
|
||||
foundExternal := sets.New[string]()
|
||||
foundPending := sets.New[string]()
|
||||
warnings := []string{}
|
||||
|
||||
// Cache endpoints to reduce redundant queries
|
||||
endpointsCache := make(map[string]*corev1.Endpoints)
|
||||
|
||||
for _, g := range gwsvcs {
|
||||
svc, f := gc.ps.ServiceIndex.HostnameAndNamespace[host.Name(g)][namespace]
|
||||
if !f {
|
||||
otherNamespaces := []string{}
|
||||
for ns := range gc.ps.ServiceIndex.HostnameAndNamespace[host.Name(g)] {
|
||||
otherNamespaces = append(otherNamespaces, `"`+ns+`"`) // Wrap in quotes for output
|
||||
}
|
||||
if len(otherNamespaces) > 0 {
|
||||
sort.Strings(otherNamespaces)
|
||||
warnings = append(warnings, fmt.Sprintf("hostname %q not found in namespace %q, but it was found in namespace(s) %v",
|
||||
g, namespace, strings.Join(otherNamespaces, ", ")))
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("hostname %q not found", g))
|
||||
}
|
||||
svc := gc.GetService(g, namespace, gvk.Service.Kind)
|
||||
if svc == nil {
|
||||
warnings = append(warnings, fmt.Sprintf("hostname %q not found", g))
|
||||
continue
|
||||
}
|
||||
svcKey := svc.Key()
|
||||
|
||||
for port := range ports {
|
||||
instances := gc.ps.ServiceInstancesByPort(svc, port, nil)
|
||||
if len(instances) > 0 {
|
||||
exists := checkServicePortExists(svc, port)
|
||||
if exists {
|
||||
foundInternal.Insert(fmt.Sprintf("%s:%d", g, port))
|
||||
if svc.Attributes.ClusterExternalAddresses.Len() > 0 {
|
||||
// Fetch external IPs from all clusters
|
||||
@@ -92,22 +97,30 @@ func (gc GatewayContext) ResolveGatewayInstances(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
instancesByPort := gc.ps.ServiceInstances(svcKey)
|
||||
if instancesEmpty(instancesByPort) {
|
||||
endpoints, ok := endpointsCache[g]
|
||||
if !ok {
|
||||
endpoints = gc.GetEndpoints(g, namespace)
|
||||
endpointsCache[g] = endpoints
|
||||
}
|
||||
|
||||
if endpoints == nil {
|
||||
warnings = append(warnings, fmt.Sprintf("no instances found for hostname %q", g))
|
||||
} else {
|
||||
hintPort := sets.New[string]()
|
||||
for _, instances := range instancesByPort {
|
||||
for _, i := range instances {
|
||||
if i.Endpoint.EndpointPort == uint32(port) {
|
||||
hintPort.Insert(strconv.Itoa(i.ServicePort.Port))
|
||||
hintWorkloadPort := false
|
||||
for _, subset := range endpoints.Subsets {
|
||||
for _, subSetPort := range subset.Ports {
|
||||
if subSetPort.Port == int32(port) {
|
||||
hintWorkloadPort = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hintWorkloadPort {
|
||||
break
|
||||
}
|
||||
}
|
||||
if hintPort.Len() > 0 {
|
||||
if hintWorkloadPort {
|
||||
warnings = append(warnings, fmt.Sprintf(
|
||||
"port %d not found for hostname %q (hint: the service port should be specified, not the workload port. Did you mean one of these ports: %v?)",
|
||||
port, g, sets.SortedList(hintPort)))
|
||||
"port %d not found for hostname %q (hint: the service port should be specified, not the workload port", port, g))
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("port %d not found for hostname %q", port, g))
|
||||
}
|
||||
@@ -119,15 +132,60 @@ func (gc GatewayContext) ResolveGatewayInstances(
|
||||
return sets.SortedList(foundInternal), sets.SortedList(foundExternal), sets.SortedList(foundPending), warnings
|
||||
}
|
||||
|
||||
func (gc GatewayContext) GetService(hostname, namespace string) *model.Service {
|
||||
return gc.ps.ServiceIndex.HostnameAndNamespace[host.Name(hostname)][namespace]
|
||||
func (gc GatewayContext) GetService(hostname, namespace, kind string) *model.Service {
|
||||
// Currently only supports type Kubernetes Service
|
||||
if kind != gvk.Service.Kind {
|
||||
log.Warnf("Unsupported kind: expected 'Service', but got '%s'", kind)
|
||||
return nil
|
||||
}
|
||||
serviceName := extractServiceName(hostname)
|
||||
|
||||
svc, err := gc.client.Kube().CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
log.Errorf("failed to get service (serviceName: %s, namespace: %s): %v", serviceName, namespace, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return serviceRegistryKube.ConvertService(*svc, gc.domainSuffix, gc.clusterID)
|
||||
}
|
||||
|
||||
func instancesEmpty(m map[int][]*model.ServiceInstance) bool {
|
||||
for _, instances := range m {
|
||||
if len(instances) > 0 {
|
||||
return false
|
||||
func (gc GatewayContext) GetEndpoints(hostname, namespace string) *corev1.Endpoints {
|
||||
serviceName := extractServiceName(hostname)
|
||||
|
||||
endpoints, err := gc.client.Kube().CoreV1().Endpoints(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
|
||||
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
log.Errorf("failed to get endpoints (serviceName: %s, namespace: %s): %v", serviceName, namespace, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func checkServicePortExists(svc *model.Service, port int) bool {
|
||||
if svc == nil {
|
||||
return false
|
||||
}
|
||||
for _, svcPort := range svc.Ports {
|
||||
if port == svcPort.Port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
func extractServiceName(hostName string) string {
|
||||
parts := strings.Split(hostName, ".")
|
||||
if len(parts) >= 4 {
|
||||
return parts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// End - Updated by Higress
|
||||
|
||||
@@ -201,7 +201,9 @@ func (c *Controller) Reconcile(ps *model.PushContext) error {
|
||||
ReferenceGrant: referenceGrant,
|
||||
DefaultGatewaySelector: c.DefaultGatewaySelector,
|
||||
Domain: c.domain,
|
||||
Context: NewGatewayContext(ps),
|
||||
// Start - Updated by Higress
|
||||
Context: NewGatewayContext(ps, c.client, c.domain, c.cluster),
|
||||
// End - Updated by Higress
|
||||
}
|
||||
|
||||
if !input.hasResources() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
|
||||
higressconfig "github.com/alibaba/higress/pkg/config"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
istio "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
@@ -1168,7 +1169,7 @@ func buildDestination(ctx configContext, to k8s.BackendRef, ns string, enforceRe
|
||||
return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
|
||||
}
|
||||
hostname := fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain)
|
||||
if ctx.Context.GetService(hostname, namespace) == nil {
|
||||
if ctx.Context.GetService(hostname, namespace, gvk.Service.Kind) == nil {
|
||||
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
|
||||
}
|
||||
return &istio.Destination{
|
||||
@@ -1192,7 +1193,7 @@ func buildDestination(ctx configContext, to k8s.BackendRef, ns string, enforceRe
|
||||
if strings.Contains(string(to.Name), ".") {
|
||||
return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
|
||||
}
|
||||
if ctx.Context.GetService(hostname, namespace) == nil {
|
||||
if ctx.Context.GetService(hostname, namespace, "ServiceImport") == nil {
|
||||
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
|
||||
}
|
||||
return &istio.Destination{
|
||||
@@ -1210,7 +1211,7 @@ func buildDestination(ctx configContext, to k8s.BackendRef, ns string, enforceRe
|
||||
return nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
|
||||
}
|
||||
hostname := string(to.Name)
|
||||
if ctx.Context.GetService(hostname, namespace) == nil {
|
||||
if ctx.Context.GetService(hostname, namespace, "Hostname") == nil {
|
||||
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
|
||||
}
|
||||
return &istio.Destination{
|
||||
@@ -1880,7 +1881,7 @@ func extractGatewayServices(r GatewayResources, kgw *k8s.GatewaySpec, obj config
|
||||
if len(name) > 0 {
|
||||
return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, false, nil
|
||||
}
|
||||
return []string{}, true, nil
|
||||
return []string{fmt.Sprintf("%s.%s.svc.%s", higressconfig.GatewayName, higressconfig.PodNamespace, util.GetDomainSuffix())}, true, nil
|
||||
}
|
||||
gatewayServices := []string{}
|
||||
skippedAddresses := []string{}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package istio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"istio.io/istio/pilot/pkg/config/kube/crd"
|
||||
credentials "istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
@@ -47,7 +49,8 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var ports = []*model.Port{
|
||||
// Start - Updated by Higress
|
||||
var ports = []corev1.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
@@ -64,232 +67,291 @@ var defaultGatewaySelector = map[string]string{
|
||||
"higress": "higress-system-higress-gateway",
|
||||
}
|
||||
|
||||
var services = []*model.Service{
|
||||
var services = []corev1.Service{
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "higress-gateway",
|
||||
Namespace: "higress-system",
|
||||
ClusterExternalAddresses: &model.AddressMap{
|
||||
Addresses: map[cluster.ID][]string{
|
||||
"Kubernetes": {"1.2.3.4"},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
ExternalIPs: []string{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example.com",
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-apple",
|
||||
Namespace: "apple",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-banana",
|
||||
Namespace: "banana",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-second",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-wildcard",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo-svc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-other",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "echo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin",
|
||||
Namespace: "cert",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-svc",
|
||||
Namespace: "service",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "google.com",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc2",
|
||||
Namespace: "allowed-1",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc2",
|
||||
Namespace: "allowed-2",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc1",
|
||||
Namespace: "allowed-1",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc3",
|
||||
Namespace: "allowed-2",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc4",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin",
|
||||
Namespace: "group-namespace1",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin",
|
||||
Namespace: "group-namespace2",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-zero",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin",
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-mirror",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-foo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-alt",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "higress-controller",
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "echo",
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin-bad",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var endpoints = []corev1.Endpoints{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "higress-gateway",
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Subsets: []corev1.EndpointSubset{
|
||||
{
|
||||
Ports: []corev1.EndpointPort{
|
||||
{
|
||||
Port: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "higress-gateway.higress-system.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "example.com",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "apple",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-apple.apple.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "banana",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-banana.banana.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-second.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-wildcard.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "foo-svc.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-other.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "example.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "echo.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "echo.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "cert",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin.cert.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "service",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "my-svc.service.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "google.com",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "allowed-1",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "svc2.allowed-1.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "allowed-2",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "svc2.allowed-2.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "allowed-1",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "svc1.allowed-1.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "allowed-2",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "svc3.allowed-2.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "svc4.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "group-namespace1",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin.group-namespace1.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "group-namespace2",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin.group-namespace2.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-zero.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin.higress-system.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-mirror.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-foo.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-alt.default.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "higress-controller.higress-system.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "higress-controller.higress-system.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "higress-system",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "echo.higress-system.svc.domain.suffix",
|
||||
},
|
||||
{
|
||||
Attributes: model.ServiceAttributes{
|
||||
Namespace: "default",
|
||||
},
|
||||
Ports: ports,
|
||||
Hostname: "httpbin-bad.default.svc.domain.suffix",
|
||||
},
|
||||
}
|
||||
|
||||
// End - Updated by Higress
|
||||
|
||||
var (
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls_test.go#L31
|
||||
rsaCertPEM = `-----BEGIN CERTIFICATE-----
|
||||
@@ -364,6 +426,21 @@ func init() {
|
||||
|
||||
func TestConvertResources(t *testing.T) {
|
||||
validator := crdvalidation.NewIstioValidator(t)
|
||||
|
||||
// Start - Updated by Higress
|
||||
client := kube.NewFakeClient()
|
||||
for _, svc := range services {
|
||||
if _, err := client.Kube().CoreV1().Services(svc.Namespace).Create(context.TODO(), &svc, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
if _, err := client.Kube().CoreV1().Endpoints(endpoint.Namespace).Create(context.TODO(), &endpoint, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// End - Updated by Higress
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
}{
|
||||
@@ -374,38 +451,23 @@ func TestConvertResources(t *testing.T) {
|
||||
{"weighted"},
|
||||
{"zero"},
|
||||
{"invalid"},
|
||||
{"multi-gateway"},
|
||||
// 目前仅支持 type 为 Hostname 和 ServiceImport
|
||||
//{"multi-gateway"},
|
||||
{"delegated"},
|
||||
{"route-binding"},
|
||||
{"reference-policy-tls"},
|
||||
{"reference-policy-service"},
|
||||
{"serviceentry"},
|
||||
//{"serviceentry"},
|
||||
{"alias"},
|
||||
{"mcs"},
|
||||
//{"mcs"},
|
||||
{"route-precedence"},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := readConfig(t, fmt.Sprintf("testdata/%s.yaml", tt.name), validator)
|
||||
// Setup a few preconfigured services
|
||||
instances := []*model.ServiceInstance{}
|
||||
for _, svc := range services {
|
||||
instances = append(instances, &model.ServiceInstance{
|
||||
Service: svc,
|
||||
ServicePort: ports[0],
|
||||
Endpoint: &model.IstioEndpoint{EndpointPort: 8080},
|
||||
}, &model.ServiceInstance{
|
||||
Service: svc,
|
||||
ServicePort: ports[1],
|
||||
Endpoint: &model.IstioEndpoint{},
|
||||
})
|
||||
}
|
||||
cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{
|
||||
Services: services,
|
||||
Instances: instances,
|
||||
})
|
||||
cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{})
|
||||
kr := splitInput(t, input)
|
||||
kr.Context = NewGatewayContext(cg.PushContext())
|
||||
kr.Context = NewGatewayContext(cg.PushContext(), client, "domain.suffix", "")
|
||||
output := convertResources(kr)
|
||||
output.AllowedReferences = AllowedReferences{} // Not tested here
|
||||
output.ReferencedNamespaceKeys = nil // Not tested here
|
||||
@@ -427,20 +489,20 @@ func TestConvertResources(t *testing.T) {
|
||||
|
||||
assert.Equal(t, golden, output)
|
||||
|
||||
//outputStatus := getStatus(t, kr.GatewayClass, kr.Gateway, kr.HTTPRoute, kr.TLSRoute, kr.TCPRoute)
|
||||
//goldenStatusFile := fmt.Sprintf("testdata/%s.status.yaml.golden", tt.name)
|
||||
//if util.Refresh() {
|
||||
// if err := os.WriteFile(goldenStatusFile, outputStatus, 0o644); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
//}
|
||||
//goldenStatus, err := os.ReadFile(goldenStatusFile)
|
||||
//if err != nil {
|
||||
// t.Fatal(err)
|
||||
//}
|
||||
//if diff := cmp.Diff(string(goldenStatus), string(outputStatus)); diff != "" {
|
||||
// t.Fatalf("Diff:\n%s", diff)
|
||||
//}
|
||||
outputStatus := getStatus(t, kr.GatewayClass, kr.Gateway, kr.HTTPRoute, kr.TLSRoute, kr.TCPRoute)
|
||||
goldenStatusFile := fmt.Sprintf("testdata/%s.status.yaml.golden", tt.name)
|
||||
if util.Refresh() {
|
||||
if err := os.WriteFile(goldenStatusFile, outputStatus, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
goldenStatus, err := os.ReadFile(goldenStatusFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(string(goldenStatus), string(outputStatus)); diff != "" {
|
||||
t.Fatalf("Diff:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -593,7 +655,7 @@ spec:
|
||||
input := readConfigString(t, tt.config, validator)
|
||||
cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{})
|
||||
kr := splitInput(t, input)
|
||||
kr.Context = NewGatewayContext(cg.PushContext())
|
||||
kr.Context = NewGatewayContext(cg.PushContext(), nil, "", "")
|
||||
output := convertResources(kr)
|
||||
c := &Controller{
|
||||
state: output,
|
||||
@@ -814,7 +876,7 @@ func BenchmarkBuildHTTPVirtualServices(b *testing.B) {
|
||||
validator := crdvalidation.NewIstioValidator(b)
|
||||
input := readConfig(b, "testdata/benchmark-httproute.yaml", validator)
|
||||
kr := splitInput(b, input)
|
||||
kr.Context = NewGatewayContext(cg.PushContext())
|
||||
kr.Context = NewGatewayContext(cg.PushContext(), nil, "", "")
|
||||
ctx := configContext{
|
||||
GatewayResources: kr,
|
||||
AllowedReferences: convertReferencePolicies(kr),
|
||||
@@ -857,7 +919,7 @@ func TestExtractGatewayServices(t *testing.T) {
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
gatewayServices: []string{},
|
||||
gatewayServices: []string{"higress-gateway.higress-system.svc.cluster.local"},
|
||||
useDefaultService: true,
|
||||
},
|
||||
{
|
||||
@@ -977,7 +1039,7 @@ func TestExtractGatewayServices(t *testing.T) {
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
gatewayServices: []string{},
|
||||
gatewayServices: []string{"higress-gateway.higress-system.svc.cluster.local"},
|
||||
useDefaultService: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -128,8 +128,7 @@ status:
|
||||
- lastTransitionTime: fake
|
||||
message: 'Failed to assign to any requested addresses: port 8080 not found for
|
||||
hostname "higress-gateway.higress-system.svc.domain.suffix" (hint: the service
|
||||
port should be specified, not the workload port. Did you mean one of these ports:
|
||||
[80]?)'
|
||||
port should be specified, not the workload port'
|
||||
reason: Invalid
|
||||
status: "False"
|
||||
type: Programmed
|
||||
@@ -163,26 +162,6 @@ status:
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: invalid-gateway-address
|
||||
namespace: invalid-gateway-address
|
||||
spec: null
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: only Hostname is supported, ignoring [1.2.3.4]
|
||||
reason: UnsupportedAddress
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Failed to assign to any requested addresses
|
||||
reason: UnsupportedAddress
|
||||
status: "False"
|
||||
type: Programmed
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: invalid-cert-kind
|
||||
@@ -477,4 +456,29 @@ status:
|
||||
namespace: higress-system
|
||||
sectionName: fake
|
||||
---
|
||||
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: no-backend
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
parents:
|
||||
- conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Route was valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: All references resolved
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: higress.io/gateway-controller
|
||||
parentRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: httpbin
|
||||
---
|
||||
|
||||
@@ -55,22 +55,23 @@ spec:
|
||||
hostname: "*.example"
|
||||
port: 8080 # Test service has port 80 with targetPort 8080
|
||||
protocol: HTTP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: invalid-gateway-address
|
||||
namespace: invalid-gateway-address
|
||||
spec:
|
||||
gatewayClassName: higress
|
||||
addresses:
|
||||
- value: 1.2.3.4
|
||||
type: istio.io/FakeType
|
||||
listeners:
|
||||
- name: default
|
||||
hostname: "*.domain.example"
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
#---
|
||||
# Higress 仅支持 addresses type 为 Hostname
|
||||
#apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
#kind: Gateway
|
||||
#metadata:
|
||||
# name: invalid-gateway-address
|
||||
# namespace: invalid-gateway-address
|
||||
#spec:
|
||||
# gatewayClassName: higress
|
||||
# addresses:
|
||||
# - value: 1.2.3.4
|
||||
# type: istio.io/FakeType
|
||||
# listeners:
|
||||
# - name: default
|
||||
# hostname: "*.domain.example"
|
||||
# port: 80
|
||||
# protocol: HTTP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: Gateway
|
||||
|
||||
@@ -53,25 +53,6 @@ spec:
|
||||
protocol: HTTP
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: Gateway/invalid-gateway-address/default.invalid-gateway-address
|
||||
creationTimestamp: null
|
||||
name: invalid-gateway-address-istio-autogenerated-k8s-gateway-default
|
||||
namespace: invalid-gateway-address
|
||||
spec:
|
||||
selector:
|
||||
higress: higress-system-higress-gateway
|
||||
servers:
|
||||
- hosts:
|
||||
- invalid-gateway-address/*.domain.example
|
||||
port:
|
||||
name: default
|
||||
number: 80
|
||||
protocol: HTTP
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
annotations:
|
||||
|
||||
@@ -920,12 +920,7 @@ func (c *controller) storeBackendTrafficPolicy(wrapper *common.WrapperConfig, ba
|
||||
if common.ValidateBackendResource(backend.Resource) && wrapper.AnnotationsConfig.Destination != nil {
|
||||
for _, dest := range wrapper.AnnotationsConfig.Destination.McpDestination {
|
||||
portNumber := dest.Destination.GetPort().GetNumber()
|
||||
serviceKey := common.ServiceKey{
|
||||
Namespace: "mcp",
|
||||
Name: dest.Destination.Host,
|
||||
Port: int32(portNumber),
|
||||
ServiceFQDN: dest.Destination.Host,
|
||||
}
|
||||
serviceKey := common.CreateMcpServiceKey(dest.Destination.Host, int32(portNumber))
|
||||
if _, exist := store[serviceKey]; !exist {
|
||||
if serviceKey.Port != 0 {
|
||||
store[serviceKey] = &common.WrapperTrafficPolicy{
|
||||
|
||||
@@ -900,12 +900,7 @@ func (c *controller) storeBackendTrafficPolicy(wrapper *common.WrapperConfig, ba
|
||||
if common.ValidateBackendResource(backend.Resource) && wrapper.AnnotationsConfig.Destination != nil {
|
||||
for _, dest := range wrapper.AnnotationsConfig.Destination.McpDestination {
|
||||
portNumber := dest.Destination.GetPort().GetNumber()
|
||||
serviceKey := common.ServiceKey{
|
||||
Namespace: "mcp",
|
||||
Name: dest.Destination.Host,
|
||||
Port: int32(portNumber),
|
||||
ServiceFQDN: dest.Destination.Host,
|
||||
}
|
||||
serviceKey := common.CreateMcpServiceKey(dest.Destination.Host, int32(portNumber))
|
||||
if _, exist := store[serviceKey]; !exist {
|
||||
if serviceKey.Port != 0 {
|
||||
store[serviceKey] = &common.WrapperTrafficPolicy{
|
||||
|
||||
@@ -20,8 +20,10 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
@@ -113,3 +115,44 @@ func BuildPatchStruct(config string) *_struct.Struct {
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
type ServiceInfo struct {
|
||||
model.NamespacedName
|
||||
Port uint32
|
||||
}
|
||||
|
||||
// convertToPort converts a port string to a uint32.
|
||||
func convertToPort(v string) (uint32, error) {
|
||||
p, err := strconv.ParseUint(v, 10, 32)
|
||||
if err != nil || p > 65535 {
|
||||
return 0, fmt.Errorf("invalid port %s: %v", v, err)
|
||||
}
|
||||
return uint32(p), nil
|
||||
}
|
||||
|
||||
func ParseServiceInfo(service string, ingressNamespace string) (ServiceInfo, error) {
|
||||
parts := strings.Split(service, ":")
|
||||
namespacedName := SplitNamespacedName(parts[0])
|
||||
|
||||
if namespacedName.Name == "" {
|
||||
return ServiceInfo{}, errors.New("service name can not be empty")
|
||||
}
|
||||
|
||||
if namespacedName.Namespace == "" {
|
||||
namespacedName.Namespace = ingressNamespace
|
||||
}
|
||||
|
||||
var port uint32
|
||||
if len(parts) == 2 {
|
||||
// If port parse fail, we ignore port and pick the first one.
|
||||
port, _ = convertToPort(parts[1])
|
||||
}
|
||||
|
||||
return ServiceInfo{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Name: namespacedName.Name,
|
||||
Namespace: namespacedName.Namespace,
|
||||
},
|
||||
Port: port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (c ServiceEntryGenerator) Generate(proxy *model.Proxy, w *model.WatchedReso
|
||||
return serviceEntries[i].CreationTimestamp.Before(serviceEntries[j].CreationTimestamp)
|
||||
})
|
||||
}
|
||||
return generate(proxy, serviceEntries, w, updates, false, false)
|
||||
return generate(proxy, serviceEntries, w, updates, c.GeneratorOptions.KeepConfigLabels, c.GeneratorOptions.KeepConfigAnnotations)
|
||||
}
|
||||
|
||||
func (c ServiceEntryGenerator) GenerateDeltas(proxy *model.Proxy, updates *model.PushRequest,
|
||||
@@ -82,7 +82,7 @@ type VirtualServiceGenerator struct {
|
||||
func (c VirtualServiceGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource,
|
||||
updates *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
|
||||
virtualServices := c.Environment.List(gvk.VirtualService, model.NamespaceAll)
|
||||
return generate(proxy, virtualServices, w, updates, false, false)
|
||||
return generate(proxy, virtualServices, w, updates, c.GeneratorOptions.KeepConfigLabels, c.GeneratorOptions.KeepConfigAnnotations)
|
||||
}
|
||||
|
||||
func (c VirtualServiceGenerator) GenerateDeltas(proxy *model.Proxy, updates *model.PushRequest,
|
||||
@@ -100,7 +100,7 @@ type DestinationRuleGenerator struct {
|
||||
func (c DestinationRuleGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource,
|
||||
updates *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
|
||||
rules := c.Environment.List(gvk.DestinationRule, model.NamespaceAll)
|
||||
return generate(proxy, rules, w, updates, false, false)
|
||||
return generate(proxy, rules, w, updates, c.GeneratorOptions.KeepConfigLabels, c.GeneratorOptions.KeepConfigAnnotations)
|
||||
}
|
||||
|
||||
func (c DestinationRuleGenerator) GenerateDeltas(proxy *model.Proxy, updates *model.PushRequest,
|
||||
@@ -118,7 +118,7 @@ type EnvoyFilterGenerator struct {
|
||||
func (c EnvoyFilterGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource,
|
||||
updates *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
|
||||
filters := c.Environment.List(gvk.EnvoyFilter, model.NamespaceAll)
|
||||
return generate(proxy, filters, w, updates, false, false)
|
||||
return generate(proxy, filters, w, updates, c.GeneratorOptions.KeepConfigLabels, c.GeneratorOptions.KeepConfigAnnotations)
|
||||
}
|
||||
|
||||
func (c EnvoyFilterGenerator) GenerateDeltas(proxy *model.Proxy, updates *model.PushRequest,
|
||||
@@ -154,7 +154,7 @@ type WasmPluginGenerator struct {
|
||||
func (c WasmPluginGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource,
|
||||
updates *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
|
||||
wasmPlugins := c.Environment.List(gvk.WasmPlugin, model.NamespaceAll)
|
||||
return generate(proxy, wasmPlugins, w, updates, false, false)
|
||||
return generate(proxy, wasmPlugins, w, updates, c.GeneratorOptions.KeepConfigLabels, c.GeneratorOptions.KeepConfigAnnotations)
|
||||
}
|
||||
|
||||
func (c WasmPluginGenerator) GenerateDeltas(proxy *model.Proxy, push *model.PushContext, updates *model.PushRequest,
|
||||
|
||||
@@ -187,10 +187,9 @@ func (m *IngressTranslation) List(typ config.GroupVersionKind, namespace string)
|
||||
higressConfig = append(higressConfig, ingressConfig...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressConfig := m.kingressConfig.List(typ, namespace)
|
||||
if kingressConfig == nil {
|
||||
return nil
|
||||
if kingressConfig != nil {
|
||||
higressConfig = append(higressConfig, kingressConfig...)
|
||||
}
|
||||
higressConfig = append(higressConfig, kingressConfig...)
|
||||
}
|
||||
return higressConfig
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
## Wasm 插件
|
||||
|
||||
|
||||
目前 Higress 提供了 c++ 和 golang 两种 Wasm 插件开发框架,支持 Wasm 插件路由&域名级匹配生效。
|
||||
|
||||
同时提供了多个内置插件,用户可以基于 Higress 提供的官方镜像仓库直接使用这些插件(以 c++ 版本举例):
|
||||
|
||||
@@ -16,15 +16,15 @@ load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
|
||||
|
||||
container_deps()
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA = "fd0be8405db25de0264bdb78fae3a82668c03782"
|
||||
PROXY_WASM_CPP_SDK_SHA = "eaec483b5b3c7bcb89fd208b5a1fa5d79d626f61"
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "c57de2425b5c61d7f630c5061e319b4557ae1f1c7526e5a51c33dc1299471b08"
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "1140bc8114d75db56a6ca6b18423d4df50d988d40b4cec929a1eb246cf5a4a3d"
|
||||
|
||||
http_archive(
|
||||
name = "proxy_wasm_cpp_sdk",
|
||||
sha256 = PROXY_WASM_CPP_SDK_SHA256,
|
||||
strip_prefix = "proxy-wasm-cpp-sdk-" + PROXY_WASM_CPP_SDK_SHA,
|
||||
url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz",
|
||||
url = "https://github.com/higress-group/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz",
|
||||
)
|
||||
|
||||
load("@proxy_wasm_cpp_sdk//bazel/dep:deps.bzl", "wasm_dependencies")
|
||||
|
||||
@@ -33,14 +33,14 @@ def wasm_libraries():
|
||||
urls = ["https://github.com/google/googletest/archive/release-1.10.0.tar.gz"],
|
||||
)
|
||||
|
||||
PROXY_WASM_CPP_HOST_SHA = "f38347360feaaf5b2a733f219c4d8c9660d626f0"
|
||||
PROXY_WASM_CPP_HOST_SHA256 = "bf10de946eb5785813895c2bf16504afc0cd590b9655d9ee52fb1074d0825ea3"
|
||||
PROXY_WASM_CPP_HOST_SHA = "7850d1721fe3dd2ccfb86a06116f76c23b1f1bf8"
|
||||
PROXY_WASM_CPP_HOST_SHA256 = "740690fc1d749849f6e24b5bc48a07dabc0565a7d03b6cd13425dba693956c57"
|
||||
|
||||
http_archive(
|
||||
name = "proxy_wasm_cpp_host",
|
||||
sha256 = PROXY_WASM_CPP_HOST_SHA256,
|
||||
strip_prefix = "proxy-wasm-cpp-host-" + PROXY_WASM_CPP_HOST_SHA,
|
||||
url = "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/" + PROXY_WASM_CPP_HOST_SHA +".tar.gz",
|
||||
url = "https://github.com/higress-group/proxy-wasm-cpp-host/archive/" + PROXY_WASM_CPP_HOST_SHA +".tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
|
||||
@@ -42,10 +42,7 @@ static RegisterContextFactory register_KeyAuth(CONTEXT_FACTORY(PluginContext),
|
||||
|
||||
namespace {
|
||||
|
||||
void deniedNoKeyAuthData(const std::string& realm) {
|
||||
sendLocalResponse(401, "No API key found in request", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}});
|
||||
}
|
||||
const std::string OriginalAuthKey("X-HI-ORIGINAL-AUTH");
|
||||
|
||||
void deniedInvalidCredentials(const std::string& realm) {
|
||||
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key",
|
||||
@@ -84,6 +81,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
Consumer c;
|
||||
auto item = consumer.find("name");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'name' field in consumer.");
|
||||
@@ -94,6 +92,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
!name.first) {
|
||||
return false;
|
||||
}
|
||||
c.name = name.first.value();
|
||||
item = consumer.find("credential");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'credential' field in consumer.");
|
||||
@@ -104,6 +103,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
!credential.first) {
|
||||
return false;
|
||||
}
|
||||
c.credential = credential.first.value();
|
||||
if (rule.credential_to_name.find(credential.first.value()) !=
|
||||
rule.credential_to_name.end()) {
|
||||
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
|
||||
@@ -113,15 +113,59 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
rule.credentials.insert(credential.first.value());
|
||||
rule.credential_to_name.emplace(
|
||||
std::make_pair(credential.first.value(), name.first.value()));
|
||||
item = consumer.find("keys");
|
||||
if (item != consumer.end()) {
|
||||
c.keys = std::vector<std::string>{OriginalAuthKey};
|
||||
if (!JsonArrayIterate(
|
||||
consumer, "keys", [&](const json& key_json) -> bool {
|
||||
auto key = JsonValueAs<std::string>(key_json);
|
||||
if (key.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
c.keys->push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for consumer keys.");
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("in_query");
|
||||
if (item != consumer.end()) {
|
||||
auto in_query = JsonValueAs<bool>(item.value());
|
||||
if (in_query.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_query.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'in_query' field in consumer "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
c.in_query = in_query.first;
|
||||
}
|
||||
item = consumer.find("in_header");
|
||||
if (item != consumer.end()) {
|
||||
auto in_header = JsonValueAs<bool>(item.value());
|
||||
if (in_header.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'in_header' field in consumer "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
c.in_header = in_header.first;
|
||||
}
|
||||
}
|
||||
rule.consumers.push_back(std::move(c));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (rule.credentials.empty()) {
|
||||
LOG_INFO("at least one credential has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
// if (rule.credentials.empty()) {
|
||||
// LOG_INFO("at least one credential has to be configured for a rule.");
|
||||
// return false;
|
||||
// }
|
||||
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
|
||||
auto key = JsonValueAs<std::string>(item);
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
@@ -137,6 +181,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
LOG_WARN("at least one key has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(OriginalAuthKey);
|
||||
auto it = configuration.find("realm");
|
||||
if (it != configuration.end()) {
|
||||
auto realm_string = JsonValueAs<std::string>(it.value());
|
||||
@@ -175,36 +220,102 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
bool PluginRootContext::checkPlugin(
|
||||
const KeyAuthConfigRule& rule,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set) {
|
||||
auto credential = extractCredential(rule);
|
||||
if (credential.empty()) {
|
||||
LOG_DEBUG("empty credential");
|
||||
deniedNoKeyAuthData(rule.realm);
|
||||
return false;
|
||||
}
|
||||
auto auth_credential_iter = rule.credentials.find(std::string(credential));
|
||||
// Check if the credential is part of the credentials
|
||||
// set from our container to grant or deny access.
|
||||
if (auth_credential_iter == rule.credentials.end()) {
|
||||
LOG_DEBUG(absl::StrCat("api key not found: ", credential));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
// Check if this credential has a consumer name. If so, check if this
|
||||
// consumer is allowed to access. If allow_set is empty, allow all consumers.
|
||||
auto credential_to_name_iter =
|
||||
rule.credential_to_name.find(std::string(std::string(credential)));
|
||||
if (credential_to_name_iter != rule.credential_to_name.end()) {
|
||||
if (allow_set && !allow_set.value().empty()) {
|
||||
if (allow_set.value().find(credential_to_name_iter->second) ==
|
||||
allow_set.value().end()) {
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
LOG_DEBUG(credential_to_name_iter->second);
|
||||
return false;
|
||||
if (rule.consumers.empty()) {
|
||||
for (const auto& key : rule.keys) {
|
||||
auto credential = extractCredential(rule.in_header, rule.in_query, key);
|
||||
if (credential.empty()) {
|
||||
LOG_DEBUG("empty credential for key: " + key);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto auth_credential_iter = rule.credentials.find(credential);
|
||||
if (auth_credential_iter == rule.credentials.end()) {
|
||||
LOG_DEBUG("api key not found: " + credential);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto credential_to_name_iter = rule.credential_to_name.find(credential);
|
||||
if (credential_to_name_iter != rule.credential_to_name.end()) {
|
||||
if (allow_set && !allow_set->empty()) {
|
||||
if (allow_set->find(credential_to_name_iter->second) ==
|
||||
allow_set->end()) {
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
LOG_DEBUG("unauthorized consumer: " +
|
||||
credential_to_name_iter->second);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
for (const auto& consumer : rule.consumers) {
|
||||
std::vector<std::string> keys_to_check =
|
||||
consumer.keys.value_or(rule.keys);
|
||||
bool in_query = consumer.in_query.value_or(rule.in_query);
|
||||
bool in_header = consumer.in_header.value_or(rule.in_header);
|
||||
|
||||
for (const auto& key : keys_to_check) {
|
||||
auto credential = extractCredential(in_header, in_query, key);
|
||||
if (credential.empty()) {
|
||||
LOG_DEBUG("empty credential for key: " + key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (credential != consumer.credential) {
|
||||
LOG_DEBUG("credential does not match the consumer's credential: " +
|
||||
credential);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto auth_credential_iter = rule.credentials.find(credential);
|
||||
if (auth_credential_iter == rule.credentials.end()) {
|
||||
LOG_DEBUG("api key not found: " + credential);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto credential_to_name_iter = rule.credential_to_name.find(credential);
|
||||
if (credential_to_name_iter != rule.credential_to_name.end()) {
|
||||
if (allow_set && !allow_set->empty()) {
|
||||
if (allow_set->find(credential_to_name_iter->second) ==
|
||||
allow_set->end()) {
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
LOG_DEBUG("unauthorized consumer: " +
|
||||
credential_to_name_iter->second);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
|
||||
}
|
||||
return true;
|
||||
|
||||
LOG_DEBUG("No valid credentials were found after checking all consumers.");
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string PluginRootContext::extractCredential(bool in_header, bool in_query,
|
||||
const std::string& key) {
|
||||
if (in_header) {
|
||||
auto header = getRequestHeader(key);
|
||||
if (header->size() != 0) {
|
||||
return header->toString();
|
||||
}
|
||||
}
|
||||
if (in_query) {
|
||||
auto request_path_header = getRequestHeader(":path");
|
||||
auto path = request_path_header->view();
|
||||
auto params = Wasm::Common::Http::parseAndDecodeQueryString(path);
|
||||
auto it = params.find(key);
|
||||
if (it != params.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
@@ -234,31 +345,6 @@ bool PluginRootContext::configure(size_t configuration_size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PluginRootContext::extractCredential(
|
||||
const KeyAuthConfigRule& rule) {
|
||||
auto request_path_header = getRequestHeader(":path");
|
||||
auto path = request_path_header->view();
|
||||
LOG_DEBUG(std::string(path));
|
||||
if (rule.in_query) {
|
||||
auto params = Wasm::Common::Http::parseAndDecodeQueryString(path);
|
||||
for (const auto& key : rule.keys) {
|
||||
auto it = params.find(key);
|
||||
if (it != params.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rule.in_header) {
|
||||
for (const auto& key : rule.keys) {
|
||||
auto header = getRequestHeader(key);
|
||||
if (header->size() != 0) {
|
||||
return header->toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkAuthRule(
|
||||
|
||||
@@ -36,7 +36,16 @@ namespace key_auth {
|
||||
|
||||
#endif
|
||||
|
||||
struct Consumer {
|
||||
std::string name;
|
||||
std::string credential;
|
||||
std::optional<std::vector<std::string>> keys;
|
||||
std::optional<bool> in_query = std::nullopt;
|
||||
std::optional<bool> in_header = std::nullopt;
|
||||
};
|
||||
|
||||
struct KeyAuthConfigRule {
|
||||
std::vector<Consumer> consumers;
|
||||
std::unordered_set<std::string> credentials;
|
||||
std::unordered_map<std::string, std::string> credential_to_name;
|
||||
std::string realm = "MSE Gateway";
|
||||
@@ -61,7 +70,8 @@ class PluginRootContext : public RootContext,
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, KeyAuthConfigRule&) override;
|
||||
std::string extractCredential(const KeyAuthConfigRule&);
|
||||
std::string extractCredential(bool in_header, bool in_query,
|
||||
const std::string& key);
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
|
||||
@@ -121,7 +121,7 @@ TEST_F(KeyAuthTest, InQuery) {
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["test"],
|
||||
"credentials":["abc"],
|
||||
"credentials":["abc","def"],
|
||||
"keys": ["apiKey", "x-api-key"]
|
||||
}
|
||||
]
|
||||
@@ -144,6 +144,10 @@ TEST_F(KeyAuthTest, InQuery) {
|
||||
path_ = "/test?hello=123&apiKey=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?hello=123&apiKey=123&x-api-key=def";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, InQueryWithConsumer) {
|
||||
@@ -173,6 +177,29 @@ TEST_F(KeyAuthTest, InQueryWithConsumer) {
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, EmptyConsumer) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [],
|
||||
"keys" : [ "apiKey", "x-api-key" ],
|
||||
"_rules_" : [ {"_match_route_" : ["test"], "allow" : []} ]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=1&apiKey=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
route_name_ = "test2";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, InHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
@@ -240,6 +267,40 @@ TEST_F(KeyAuthTest, InHeaderWithConsumer) {
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, ConsumerDifferentKey) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [ {"credential" : "abc", "name" : "consumer1", "keys" : [ "apiKey" ]}, {"credential" : "123", "name" : "consumer2"} ],
|
||||
"keys" : [ "apiKey2" ],
|
||||
"_rules_" : [ {"_match_route_" : ["test"], "allow" : ["consumer1"]}, {"_match_route_" : ["test2"], "allow" : ["consumer2"]} ]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=1&apiKey=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=1&apiKey2=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=123&apiKey2=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
route_name_ = "test2";
|
||||
path_ = "/test?hello=123&apiKey2=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace key_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
70
plugins/wasm-cpp/extensions/model_router/BUILD
Normal file
70
plugins/wasm-cpp/extensions/model_router/BUILD
Normal file
@@ -0,0 +1,70 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "model_router.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
],
|
||||
deps = [
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics_higress",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "model_router_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util_nullvm",
|
||||
"//common:rule_util_nullvm",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "model_router_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":model_router_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "model_router",
|
||||
wasm_file = ":model_router.wasm",
|
||||
)
|
||||
64
plugins/wasm-cpp/extensions/model_router/README.md
Normal file
64
plugins/wasm-cpp/extensions/model_router/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
## 功能说明
|
||||
`model-router`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`260`
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `enable` | bool | 选填 | false | 是否开启基于model参数路由 |
|
||||
| `model_key` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `add_header_key` | string | 选填 | x-higress-llm-provider | 从model参数中解析出的provider名字放到哪个请求header中 |
|
||||
|
||||
|
||||
## 效果说明
|
||||
|
||||
如下开启基于model参数路由的功能:
|
||||
|
||||
```yaml
|
||||
enable: true
|
||||
```
|
||||
|
||||
开启后,插件将请求中 model 参数的 provider 部分(如果有)提取出来,设置到 x-higress-llm-provider 这个请求 header 中,用于后续路由,并将 model 参数重写为模型名称部分。举例来说,原生的 LLM 请求体是:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen/qwen-long",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "higress项目主仓库的github地址是什么"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
经过这个插件后,将添加下面这个请求头(可以用于路由匹配):
|
||||
|
||||
x-higress-llm-provider: qwen
|
||||
|
||||
原始的 LLM 请求体将被改成:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen-long",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "higress项目主仓库的github地址是什么"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
63
plugins/wasm-cpp/extensions/model_router/README_EN.md
Normal file
63
plugins/wasm-cpp/extensions/model_router/README_EN.md
Normal file
@@ -0,0 +1,63 @@
|
||||
## Function Description
|
||||
The `model-router` plugin implements the functionality of routing based on the `model` parameter in the LLM protocol.
|
||||
|
||||
## Runtime Properties
|
||||
|
||||
Plugin Execution Phase: `Default Phase`
|
||||
Plugin Execution Priority: `260`
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Name | Data Type | Filling Requirement | Default Value | Description |
|
||||
| -------------------- | ------------- | --------------------- | ---------------------- | ----------------------------------------------------- |
|
||||
| `enable` | bool | Optional | false | Whether to enable routing based on the `model` parameter |
|
||||
| `model_key` | string | Optional | model | The location of the `model` parameter in the request body |
|
||||
| `add_header_key` | string | Optional | x-higress-llm-provider | The header where the parsed provider name from the `model` parameter will be placed |
|
||||
|
||||
## Effect Description
|
||||
|
||||
To enable routing based on the `model` parameter, use the following configuration:
|
||||
|
||||
```yaml
|
||||
enable: true
|
||||
```
|
||||
|
||||
After enabling, the plugin extracts the provider part (if any) from the `model` parameter in the request, and sets it in the `x-higress-llm-provider` request header for subsequent routing. It also rewrites the `model` parameter to the model name part. For example, the original LLM request body is:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "openai/gpt-4o",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address for the main repository of the Higress project?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
After processing by the plugin, the following request header (which can be used for routing matching) will be added:
|
||||
|
||||
`x-higress-llm-provider: openai`
|
||||
|
||||
The original LLM request body will be modified to:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address for the main repository of the Higress project?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
189
plugins/wasm-cpp/extensions/model_router/plugin.cc
Normal file
189
plugins/wasm-cpp/extensions/model_router/plugin.cc
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "extensions/model_router/plugin.h"
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/http_util.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace model_router {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_ModelRouter(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view SetDecoderBufferLimitKey =
|
||||
"SetRequestBodyBufferLimit";
|
||||
constexpr std::string_view DefaultMaxBodyBytes = "10485760";
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
ModelRouterConfigRule& rule) {
|
||||
if (auto it = configuration.find("enable"); it != configuration.end()) {
|
||||
if (it->is_boolean()) {
|
||||
rule.enable_ = it->get<bool>();
|
||||
} else {
|
||||
LOG_WARN("Invalid type for enable. Expected boolean.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto it = configuration.find("model_key"); it != configuration.end()) {
|
||||
if (it->is_string()) {
|
||||
rule.model_key_ = it->get<std::string>();
|
||||
} else {
|
||||
LOG_WARN("Invalid type for model_key. Expected string.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto it = configuration.find("add_header_key");
|
||||
it != configuration.end()) {
|
||||
if (it->is_string()) {
|
||||
rule.add_header_key_ = it->get<std::string>();
|
||||
} else {
|
||||
LOG_WARN("Invalid type for add_header_key. Expected string.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginRootContext::onHeader(
|
||||
const ModelRouterConfigRule& rule) {
|
||||
if (!rule.enable_ || !Wasm::Common::Http::hasRequestBody()) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto content_type_value =
|
||||
getRequestHeader(Wasm::Common::Http::Header::ContentType);
|
||||
if (!absl::StrContains(content_type_value->view(),
|
||||
Wasm::Common::Http::ContentTypeValues::Json)) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
|
||||
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
FilterDataStatus PluginRootContext::onBody(const ModelRouterConfigRule& rule,
|
||||
std::string_view body) {
|
||||
const auto& model_key = rule.model_key_;
|
||||
const auto& add_header_key = rule.add_header_key_;
|
||||
auto body_json_opt = ::Wasm::Common::JsonParse(body);
|
||||
if (!body_json_opt) {
|
||||
LOG_WARN(absl::StrCat("cannot parse body to JSON string: ", body));
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
auto body_json = body_json_opt.value();
|
||||
if (body_json.contains(model_key)) {
|
||||
std::string model_value = body_json[model_key];
|
||||
auto pos = model_value.find('/');
|
||||
if (pos != std::string::npos) {
|
||||
const auto& provider = model_value.substr(0, pos);
|
||||
const auto& model = model_value.substr(pos + 1);
|
||||
replaceRequestHeader(add_header_key, provider);
|
||||
body_json[model_key] = model;
|
||||
setBuffer(WasmBufferType::HttpRequestBody, 0,
|
||||
std::numeric_limits<size_t>::max(), body_json.dump());
|
||||
LOG_DEBUG(absl::StrCat("model route to provider:", provider,
|
||||
", model:", model));
|
||||
} else {
|
||||
LOG_DEBUG(absl::StrCat("model route not work, model:", model_value));
|
||||
}
|
||||
}
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->onHeaders([rootCtx, this](const auto& config) {
|
||||
auto ret = rootCtx->onHeader(config);
|
||||
if (ret == FilterHeadersStatus::StopIteration) {
|
||||
this->config_ = &config;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
FilterDataStatus PluginContext::onRequestBody(size_t body_size,
|
||||
bool end_stream) {
|
||||
if (config_ == nullptr) {
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
body_total_size_ += body_size;
|
||||
if (!end_stream) {
|
||||
return FilterDataStatus::StopIterationAndBuffer;
|
||||
}
|
||||
auto body =
|
||||
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->onBody(*config_, body->view());
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace model_router
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
85
plugins/wasm-cpp/extensions/model_router/plugin.h
Normal file
85
plugins/wasm-cpp/extensions/model_router/plugin.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace model_router {
|
||||
|
||||
#endif
|
||||
|
||||
struct ModelRouterConfigRule {
|
||||
bool enable_ = false;
|
||||
std::string model_key_ = "model";
|
||||
std::string add_header_key_ = "x-higress-llm-provider";
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<ModelRouterConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
FilterHeadersStatus onHeader(const ModelRouterConfigRule&);
|
||||
FilterDataStatus onBody(const ModelRouterConfigRule&, std::string_view);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, ModelRouterConfigRule&) override;
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
FilterDataStatus onRequestBody(size_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
|
||||
size_t body_total_size_ = 0;
|
||||
const ModelRouterConfigRule* config_ = nullptr;
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace model_router
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
192
plugins/wasm-cpp/extensions/model_router/plugin_test.cc
Normal file
192
plugins/wasm-cpp/extensions/model_router/plugin_test.cc
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "extensions/model_router/plugin.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace model_router {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_model_router_plugin("model_router", []() {
|
||||
return std::make_unique<NullPlugin>(model_router::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, setBuffer,
|
||||
(WasmBufferType, size_t, size_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, replaceHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view /* value */));
|
||||
MOCK_METHOD(WasmResult, removeHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */));
|
||||
MOCK_METHOD(WasmResult, addHeaderMapValue,
|
||||
(WasmHeaderMapType, std::string_view, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
MOCK_METHOD(WasmResult, setProperty, (std::string_view, std::string_view));
|
||||
};
|
||||
class ModelRouterTest : public ::testing::Test {
|
||||
protected:
|
||||
ModelRouterTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("model_router");
|
||||
wasm_base_->initialize();
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getBuffer(testing::_))
|
||||
.WillByDefault([&](WasmBufferType type) {
|
||||
if (type == WasmBufferType::HttpRequestBody) {
|
||||
return &body_;
|
||||
}
|
||||
return &config_;
|
||||
});
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == "content-type") {
|
||||
*result = "application/json";
|
||||
} else if (header == "content-length") {
|
||||
*result = "1024";
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_,
|
||||
replaceHeaderMapValue(WasmHeaderMapType::RequestHeaders, testing::_,
|
||||
testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view key,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
ON_CALL(*mock_context_,
|
||||
removeHeaderMapValue(WasmHeaderMapType::RequestHeaders, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view key) {
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_, setProperty(testing::_, testing::_))
|
||||
.WillByDefault(
|
||||
[&](std::string_view, std::string_view) { return WasmResult::Ok; });
|
||||
}
|
||||
~ModelRouterTest() override {}
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
std::string route_name_;
|
||||
BufferBase body_;
|
||||
BufferBase config_;
|
||||
};
|
||||
|
||||
TEST_F(ModelRouterTest, RewriteModelAndHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"enable": true
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
std::string request_json = R"({"model": "qwen/qwen-long"})";
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.WillOnce([&](WasmBufferType, size_t, size_t, std::string_view body) {
|
||||
EXPECT_EQ(body, R"({"model":"qwen-long"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mock_context_,
|
||||
replaceHeaderMapValue(testing::_,
|
||||
std::string_view("x-higress-llm-provider"),
|
||||
std::string_view("qwen")));
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["route-a"],
|
||||
"enable": true
|
||||
}
|
||||
]})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
std::string request_json = R"({"model": "qwen/qwen-long"})";
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.WillOnce([&](WasmBufferType, size_t, size_t, std::string_view body) {
|
||||
EXPECT_EQ(body, R"({"model":"qwen-long"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mock_context_,
|
||||
replaceHeaderMapValue(testing::_,
|
||||
std::string_view("x-higress-llm-provider"),
|
||||
std::string_view("qwen")));
|
||||
|
||||
body_.set(request_json);
|
||||
route_name_ = "route-a";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace model_router
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
@@ -55,6 +55,9 @@ output wasm file: extensions/request-block/plugin.wasm
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./extensions/request-block/main.go
|
||||
```
|
||||
|
||||
详细的编译说明,包括要使用更复杂的 Header 状态管理机制,请参考[ Go 开发插件的最佳实践](https://higress.io/docs/latest/user/wasm-go/#3-%E7%BC%96%E8%AF%91%E7%94%9F%E6%88%90-wasm-%E6%96%87%E4%BB%B6)。
|
||||
|
||||
|
||||
### step2. 构建并推送插件的 docker 镜像
|
||||
|
||||
使用这份简单的 Dockerfile
|
||||
@@ -201,4 +204,4 @@ cSuite.Setup(t)
|
||||
|
||||
```bash
|
||||
PLUGIN_NAME=request-block make higress-wasmplugin-test
|
||||
```
|
||||
```
|
||||
|
||||
@@ -5,14 +5,13 @@ description: AI Agent插件配置参考
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
一个可定制化的 API AI Agent,支持配置 http method 类型为 GET 与 POST 的 API,目前只支持非流式模式。
|
||||
一个可定制化的 API AI Agent,支持配置 http method 类型为 GET 与 POST 的 API,支持多轮对话,支持流式与非流式模式。
|
||||
agent流程图如下:
|
||||

|
||||

|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`20`
|
||||
插件执行优先级:`200`
|
||||
|
||||
## 配置字段
|
||||
|
||||
@@ -46,18 +45,19 @@ agent流程图如下:
|
||||
|
||||
`apiProvider`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|---------|--------|------------------------------------------|
|
||||
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
|
||||
| `serviceName` | string | 必填 | - | 访问外部 API 服务名 |
|
||||
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
|
||||
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|-----------|---------|--------|------------------------------------------|
|
||||
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
|
||||
| `maxExecutionTime`| int | 非必填 | 50000 | 每一次请求API的超时时间,单位毫秒。 |
|
||||
| `serviceName` | string | 必填 | - | 访问外部 API 服务名 |
|
||||
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
|
||||
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
|
||||
|
||||
`apiKey`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|---------|------------|--------|-------------------------------------------------------------------------------|
|
||||
| `in` | string | 非必填 | header | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,默认是 header。
|
||||
|-------------------|---------|------------|--------|-----------------------------------------------------------------------------------------|
|
||||
| `in` | string | 非必填 | none | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,如果API没有令牌,填none。
|
||||
| `name` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的名称。 |
|
||||
| `value` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的值。 |
|
||||
|
||||
@@ -75,11 +75,8 @@ agent流程图如下:
|
||||
|-----------------|-----------|-----------|--------|---------------------------------------------|
|
||||
| `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 部分 |
|
||||
|
||||
## 用法示例
|
||||
|
||||
@@ -325,6 +322,21 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
|
||||
**请求示例**
|
||||
|
||||
```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": "济南的天气如何?"},{ "role": "assistant","content": "目前,济南市的天气为多云,气温为24℃,数据更新时间为2024年9月12日21时50分14秒。"},{"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":"目前,北京市的天气为晴朗,气温为19℃,数据更新时间为2024年9月12日22时17分40秒。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":999,"completion_tokens":76,"total_tokens":1075}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
|
||||
@@ -4,13 +4,13 @@ keywords: [ AI Gateway, AI Agent ]
|
||||
description: AI Agent plugin configuration reference
|
||||
---
|
||||
## Functional Description
|
||||
A customizable API AI Agent that supports configuring HTTP method types as GET and POST APIs. Currently, it only supports non-streaming mode.
|
||||
A customizable API AI Agent that supports configuring HTTP method types as GET and POST APIs. Supports multiple dialogue rounds, streaming and non-streaming modes.
|
||||
The agent flow chart is as follows:
|
||||

|
||||
|
||||
## Runtime Properties
|
||||
Plugin execution phase: `Default Phase`
|
||||
Plugin execution priority: `20`
|
||||
Plugin execution priority: `200`
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
@@ -41,17 +41,18 @@ The configuration fields for `apis` are as follows:
|
||||
| `api` | string | Required | - | OpenAPI documentation of the tool |
|
||||
|
||||
The configuration fields for `apiProvider` are as follows:
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
|-----------------|-----------|-------------|---------------|--------------------------------------------------|
|
||||
| `apiKey` | object | Optional | - | Token for authentication when accessing external API services. |
|
||||
| `serviceName` | string | Required | - | Name of the external API service |
|
||||
| `servicePort` | int | Required | - | Port of the external API service |
|
||||
| `domain` | string | Required | - | Domain for accessing the external API |
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
|-------------------|-----------|-------------|---------------|--------------------------------------------------|
|
||||
| `apiKey` | object | Optional | - | Token for authentication when accessing external API services. |
|
||||
| `maxExecutionTime`| int | Optional | 50000 | Timeout for each request to the API, in milliseconds|
|
||||
| `serviceName` | string | Required | - | Name of the external API service |
|
||||
| `servicePort` | int | Required | - | Port of the external API service |
|
||||
| `domain` | string | Required | - | Domain for accessing the external API |
|
||||
|
||||
The configuration fields for `apiKey` are as follows:
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
|-------------------|-----------|-------------|---------------|-------------------------------------------------------------------------------------|
|
||||
| `in` | string | Optional | header | Whether the authentication token for accessing the external API service is in the header or in the query; default is header. |
|
||||
| `in` | string | Optional | none | Whether the authentication token for accessing the external API service is in the header or in the query; If the API does not have a token, fill in none. |
|
||||
| `name` | string | Optional | - | The name of the token for authentication when accessing the external API service. |
|
||||
| `value` | string | Optional | - | The value of the token for authentication when accessing the external API service. |
|
||||
|
||||
@@ -67,11 +68,8 @@ The configuration fields for `chTemplate` and `enTemplate` are as follows:
|
||||
|-----------------|-----------|-------------|---------------|---------------------------------------------------|
|
||||
| `question` | string | Optional | - | The question part of the Agent ReAct template |
|
||||
| `thought1` | string | Optional | - | The thought1 part of the Agent ReAct template |
|
||||
| `actionInput` | string | Optional | - | The actionInput part of the Agent ReAct template |
|
||||
| `observation` | string | Optional | - | The observation part of the Agent ReAct template |
|
||||
| `thought2` | string | Optional | - | The thought2 part of the Agent ReAct template |
|
||||
| `finalAnswer` | string | Optional | - | The finalAnswer part of the Agent ReAct template |
|
||||
| `begin` | string | Optional | - | The begin part of the Agent ReAct template |
|
||||
|
||||
## Usage Example
|
||||
**Configuration Information**
|
||||
@@ -308,6 +306,17 @@ curl 'http://<replace with gateway public IP>/api/openai/v1/chat/completions' \
|
||||
curl 'http://<replace with gateway public 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":"What is the current weather in Jinan?"},{"role":"assistant","content":" The current weather condition in Jinan is overcast, with a temperature of 31°C. This information was last updated on August 9, 2024, at 15:12 (Beijing time)."},{"role":"user","content":"BeiJing?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
**Response Example**
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" The current weather condition in Beijing is overcast, with a temperature of 19°C. This information was last updated on Sep 12, 2024, at 22:17 (Beijing time)."},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":999,"completion_tokens":76,"total_tokens":1075}}
|
||||
```
|
||||
**Request Example**
|
||||
```shell
|
||||
curl 'http://<replace with gateway public 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":"What is the current weather in Jinan? Please indicate in Fahrenheit and respond in Japanese."}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
**Response Example**
|
||||
|
||||
@@ -46,7 +46,7 @@ type Response struct {
|
||||
}
|
||||
|
||||
// 用于存放拆解出来的工具相关信息
|
||||
type Tool_Param struct {
|
||||
type ToolsParam struct {
|
||||
ToolName string `yaml:"toolName"`
|
||||
Path string `yaml:"path"`
|
||||
Method string `yaml:"method"`
|
||||
@@ -56,10 +56,11 @@ type Tool_Param struct {
|
||||
}
|
||||
|
||||
// 用于存放拆解出来的api相关信息
|
||||
type APIParam struct {
|
||||
APIKey APIKey `yaml:"apiKey"`
|
||||
URL string `yaml:"url"`
|
||||
Tool_Param []Tool_Param `yaml:"tool_Param"`
|
||||
type APIsParam struct {
|
||||
APIKey APIKey `yaml:"apiKey"`
|
||||
URL string `yaml:"url"`
|
||||
MaxExecutionTime int64 `yaml:"maxExecutionTime"`
|
||||
ToolsParam []ToolsParam `yaml:"toolsParam"`
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
@@ -153,7 +154,10 @@ type APIProvider struct {
|
||||
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"`
|
||||
Domain string `required:"true" yaml:"domain" json:"domain"`
|
||||
// @Title zh-CN 每一次请求api的超时时间
|
||||
// @Description zh-CN 每一次请求api的超时时间,单位毫秒,默认50000
|
||||
MaxExecutionTime int64 `yaml:"maxExecutionTime" json:"maxExecutionTime"`
|
||||
// @Title zh-CN 通义千问大模型服务的key
|
||||
// @Description zh-CN 通义千问大模型服务的key
|
||||
APIKey APIKey `required:"true" yaml:"apiKey" json:"apiKey"`
|
||||
@@ -167,11 +171,8 @@ type APIs struct {
|
||||
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 {
|
||||
@@ -189,7 +190,7 @@ type LLMInfo struct {
|
||||
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"`
|
||||
Domain string `required:"true" yaml:"domain" json:"domain"`
|
||||
// @Title zh-CN 大模型服务的key
|
||||
// @Description zh-CN 大模型服务的key
|
||||
APIKey string `required:"true" yaml:"apiKey" json:"apiKey"`
|
||||
@@ -222,7 +223,7 @@ type PluginConfig struct {
|
||||
// @Description zh-CN 用于存储llm使用信息
|
||||
LLMInfo LLMInfo `required:"true" yaml:"llm" json:"llm"`
|
||||
LLMClient wrapper.HttpClient `yaml:"-" json:"-"`
|
||||
APIParam []APIParam `yaml:"-" json:"-"`
|
||||
APIsParam []APIsParam `yaml:"-" json:"-"`
|
||||
PromptTemplate PromptTemplate `yaml:"promptTemplate" json:"promptTemplate"`
|
||||
}
|
||||
|
||||
@@ -260,11 +261,13 @@ func initAPIs(gjson gjson.Result, c *PluginConfig) error {
|
||||
return errors.New("apiProvider domain is required")
|
||||
}
|
||||
|
||||
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
|
||||
if apiKeyIn != "query" {
|
||||
apiKeyIn = "header"
|
||||
maxExecutionTime := item.Get("apiProvider.maxExecutionTime").Int()
|
||||
if maxExecutionTime == 0 {
|
||||
maxExecutionTime = 50000
|
||||
}
|
||||
|
||||
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
|
||||
|
||||
apiKeyName := item.Get("apiProvider.apiKey.name")
|
||||
|
||||
apiKeyValue := item.Get("apiProvider.apiKey.value")
|
||||
@@ -289,13 +292,13 @@ func initAPIs(gjson gjson.Result, c *PluginConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var allTool_param []Tool_Param
|
||||
var allTool_param []ToolsParam
|
||||
//拆除服务下面的每个api的path
|
||||
for path, pathmap := range apiStruct.Paths {
|
||||
//拆解出每个api对应的参数
|
||||
for method, submap := range pathmap {
|
||||
//把参数列表存起来
|
||||
var param Tool_Param
|
||||
var param ToolsParam
|
||||
param.Path = path
|
||||
param.ToolName = submap.OperationID
|
||||
if method == "get" {
|
||||
@@ -319,13 +322,14 @@ func initAPIs(gjson gjson.Result, c *PluginConfig) error {
|
||||
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,
|
||||
apiParam := APIsParam{
|
||||
APIKey: APIKey{In: apiKeyIn, Name: apiKeyName.String(), Value: apiKeyValue.String()},
|
||||
URL: apiStruct.Servers[0].URL,
|
||||
MaxExecutionTime: maxExecutionTime,
|
||||
ToolsParam: allTool_param,
|
||||
}
|
||||
|
||||
c.APIParam = append(c.APIParam, apiParam)
|
||||
c.APIsParam = append(c.APIsParam, apiParam)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -338,60 +342,36 @@ func initReActPromptTpl(gjson gjson.Result, c *PluginConfig) {
|
||||
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.Question = "input question to 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.Thought1 = "consider previous and subsequent steps"
|
||||
}
|
||||
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.Observation = "action result"
|
||||
}
|
||||
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"
|
||||
c.PromptTemplate.ENTemplate.Thought2 = gjson.Get("promptTemplate.enTemplate.thought2").String()
|
||||
if c.PromptTemplate.ENTemplate.Thought2 == "" {
|
||||
c.PromptTemplate.ENTemplate.Thought2 = "I know what to respond"
|
||||
}
|
||||
} 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.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.Thought1 = "考虑之前和之后的步骤"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Observation = gjson.Get("promptTemplate.chTemplate.observation").String()
|
||||
if c.PromptTemplate.CHTemplate.Observation == "" {
|
||||
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 = "再次重申,不要修改以上模板的字段名称,开始吧!"
|
||||
c.PromptTemplate.CHTemplate.Thought2 = gjson.Get("promptTemplate.chTemplate.thought2").String()
|
||||
if c.PromptTemplate.CHTemplate.Thought2 == "" {
|
||||
c.PromptTemplate.CHTemplate.Thought2 = "我知道该回应什么"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +380,7 @@ 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.Domain = 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()
|
||||
@@ -419,6 +399,6 @@ func initLLMClient(gjson gjson.Result, c *PluginConfig) {
|
||||
c.LLMClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.LLMInfo.ServiceName,
|
||||
Port: c.LLMInfo.ServicePort,
|
||||
Host: c.LLMInfo.Domin,
|
||||
Host: c.LLMInfo.Domain,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
// 用于统计函数的递归调用次数
|
||||
const ToolCallsCount = "ToolCallsCount"
|
||||
const StreamContextKey = "Stream"
|
||||
|
||||
// react的正则规则
|
||||
const ActionPattern = `Action:\s*(.*?)[.\n]`
|
||||
@@ -53,7 +54,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
|
||||
func firstReq(ctx wrapper.HttpContext, config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
|
||||
log.Debugf("[onHttpRequestBody] firstreq:%s", prompt)
|
||||
|
||||
var userMessage Message
|
||||
@@ -62,13 +63,17 @@ func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrappe
|
||||
|
||||
newMessages := []Message{userMessage}
|
||||
rawRequest.Messages = newMessages
|
||||
if rawRequest.Stream {
|
||||
ctx.SetContext(StreamContextKey, struct{}{})
|
||||
rawRequest.Stream = false
|
||||
}
|
||||
|
||||
//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))
|
||||
log.Debugf("[onHttpRequestBody] newRequestBody: %s", string(newbody))
|
||||
err := proxywasm.ReplaceHttpRequestBody(newbody)
|
||||
if err != nil {
|
||||
log.Debug("替换失败")
|
||||
@@ -87,18 +92,26 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
var rawRequest Request
|
||||
err := json.Unmarshal(body, &rawRequest)
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] body json umarshal err: ", err.Error())
|
||||
log.Debugf("[onHttpRequestBody] body json umarshal err: %s", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Debugf("onHttpRequestBody rawRequest: %v", rawRequest)
|
||||
|
||||
//获取用户query
|
||||
var query string
|
||||
var history string
|
||||
messageLength := len(rawRequest.Messages)
|
||||
log.Debugf("[onHttpRequestBody] messageLength: %s\n", messageLength)
|
||||
log.Debugf("[onHttpRequestBody] messageLength: %s", messageLength)
|
||||
if messageLength > 0 {
|
||||
query = rawRequest.Messages[messageLength-1].Content
|
||||
log.Debugf("[onHttpRequestBody] query: %s\n", query)
|
||||
log.Debugf("[onHttpRequestBody] query: %s", query)
|
||||
if messageLength >= 3 {
|
||||
for i := 0; i < messageLength-1; i += 2 {
|
||||
history += "human: " + rawRequest.Messages[i].Content + "\nAI: " + rawRequest.Messages[i+1].Content
|
||||
}
|
||||
} else {
|
||||
history = ""
|
||||
}
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -111,8 +124,8 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
//拼装agent prompt模板
|
||||
tool_desc := make([]string, 0)
|
||||
tool_names := make([]string, 0)
|
||||
for _, apiParam := range config.APIParam {
|
||||
for _, tool_param := range apiParam.Tool_Param {
|
||||
for _, apisParam := range config.APIsParam {
|
||||
for _, tool_param := range apisParam.ToolsParam {
|
||||
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)
|
||||
}
|
||||
@@ -122,26 +135,22 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
if config.PromptTemplate.Language == "CH" {
|
||||
prompt = fmt.Sprintf(prompttpl.CH_Template,
|
||||
tool_desc,
|
||||
tool_names,
|
||||
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,
|
||||
history,
|
||||
query)
|
||||
} else {
|
||||
prompt = fmt.Sprintf(prompttpl.EN_Template,
|
||||
tool_desc,
|
||||
tool_names,
|
||||
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,
|
||||
history,
|
||||
query)
|
||||
}
|
||||
|
||||
@@ -154,7 +163,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
dashscope.MessageStore.AddForUser(prompt)
|
||||
|
||||
//开始第一次请求
|
||||
ret := firstReq(config, prompt, rawRequest, log)
|
||||
ret := firstReq(ctx, config, prompt, rawRequest, log)
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -168,7 +177,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wra
|
||||
|
||||
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.Debugf("statusCode: %d", statusCode)
|
||||
}
|
||||
log.Info("========函数返回结果========")
|
||||
log.Infof(string(responseBody))
|
||||
@@ -193,30 +202,36 @@ func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content strin
|
||||
//得到gpt的返回结果
|
||||
var responseCompletion dashscope.CompletionResponse
|
||||
_ = json.Unmarshal(responseBody, &responseCompletion)
|
||||
log.Infof("[toolsCall] content: %s\n", responseCompletion.Choices[0].Message.Content)
|
||||
log.Infof("[toolsCall] content: %s", responseCompletion.Choices[0].Message.Content)
|
||||
|
||||
if responseCompletion.Choices[0].Message.Content != "" {
|
||||
retType := toolsCall(ctx, config, responseCompletion.Choices[0].Message.Content, rawResponse, log)
|
||||
retType, actionInput := 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
|
||||
}
|
||||
if ctx.GetContext(StreamContextKey) == nil {
|
||||
assistantMessage.Role = "assistant"
|
||||
assistantMessage.Content = actionInput
|
||||
rawResponse.Choices[0].Message = assistantMessage
|
||||
newbody, err := json.Marshal(rawResponse)
|
||||
if err != nil {
|
||||
proxywasm.ResumeHttpResponse()
|
||||
return
|
||||
} else {
|
||||
proxywasm.ReplaceHttpResponseBody(newbody)
|
||||
|
||||
rawResponse.Choices[0].Message = assistantMessage
|
||||
|
||||
newbody, err := json.Marshal(rawResponse)
|
||||
if err != nil {
|
||||
proxywasm.ResumeHttpResponse()
|
||||
return
|
||||
log.Debug("[onHttpResponseBody] response替换成功")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}
|
||||
} else {
|
||||
log.Infof("[onHttpResponseBody] newResponseBody: ", string(newbody))
|
||||
proxywasm.ReplaceHttpResponseBody(newbody)
|
||||
headers := [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}
|
||||
proxywasm.ReplaceHttpResponseHeaders(headers)
|
||||
// Remove quotes from actionInput
|
||||
actionInput = strings.Trim(actionInput, "\"")
|
||||
returnStreamResponseTemplate := `data:{"id":"%s","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"%s","object":"chat.completion","usage":{"prompt_tokens":%d,"completion_tokens":%d,"total_tokens":%d}}` + "\n\ndata:[DONE]\n\n"
|
||||
newbody := fmt.Sprintf(returnStreamResponseTemplate, rawResponse.ID, actionInput, rawResponse.Model, rawResponse.Usage.PromptTokens, rawResponse.Usage.CompletionTokens, rawResponse.Usage.TotalTokens)
|
||||
log.Infof("[onHttpResponseBody] newResponseBody: ", newbody)
|
||||
proxywasm.ReplaceHttpResponseBody([]byte(newbody))
|
||||
|
||||
log.Debug("[onHttpResponseBody] response替换成功")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
@@ -232,121 +247,156 @@ func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content strin
|
||||
}
|
||||
}
|
||||
|
||||
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) types.Action {
|
||||
func outputParser(response string, log wrapper.Log) (string, string) {
|
||||
log.Debugf("Raw response:%s", response)
|
||||
|
||||
start := strings.Index(response, "```")
|
||||
end := strings.LastIndex(response, "```")
|
||||
|
||||
var jsonStr string
|
||||
if start != -1 && end != -1 {
|
||||
jsonStr = strings.TrimSpace(response[start+3 : end])
|
||||
} else {
|
||||
jsonStr = response
|
||||
}
|
||||
|
||||
log.Debugf("Extracted JSON string:%s", jsonStr)
|
||||
|
||||
var action map[string]interface{}
|
||||
err := json.Unmarshal([]byte(jsonStr), &action)
|
||||
if err == nil {
|
||||
var actionName, actionInput string
|
||||
for key, value := range action {
|
||||
if strings.Contains(strings.ToLower(key), "input") {
|
||||
actionInput = fmt.Sprintf("%v", value)
|
||||
} else {
|
||||
actionName = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
if actionName != "" && actionInput != "" {
|
||||
return actionName, actionInput
|
||||
}
|
||||
}
|
||||
log.Debugf("json parse err: %s", err.Error())
|
||||
// Fallback to regex parsing if JSON unmarshaling fails
|
||||
pattern := `\{\s*"action":\s*"([^"]+)",\s*"action_input":\s*((?:\{[^}]+\})|"[^"]+")\s*\}`
|
||||
re := regexp.MustCompile(pattern)
|
||||
match := re.FindStringSubmatch(jsonStr)
|
||||
|
||||
if len(match) == 3 {
|
||||
action := match[1]
|
||||
actionInput := match[2]
|
||||
log.Debugf("Parsed action:%s, action_input:%s", action, actionInput)
|
||||
return action, actionInput
|
||||
}
|
||||
|
||||
log.Debug("No valid action and action_input found in the response")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) (types.Action, string) {
|
||||
dashscope.MessageStore.AddForAssistant(content)
|
||||
|
||||
action, actionInput := outputParser(content, log)
|
||||
|
||||
//得到最终答案
|
||||
regexPattern := regexp.MustCompile(FinalAnswerPattern)
|
||||
finalAnswer := regexPattern.FindStringSubmatch(content)
|
||||
if len(finalAnswer) > 1 {
|
||||
return types.ActionContinue
|
||||
if action == "Final Answer" {
|
||||
return types.ActionContinue, actionInput
|
||||
}
|
||||
count := ctx.GetContext(ToolCallsCount).(int)
|
||||
count++
|
||||
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d\n", count, config.LLMInfo.MaxIterations)
|
||||
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d", count, config.LLMInfo.MaxIterations)
|
||||
//函数递归调用次数,达到了预设的循环次数,强制结束
|
||||
if int64(count) > config.LLMInfo.MaxIterations {
|
||||
ctx.SetContext(ToolCallsCount, 0)
|
||||
return types.ActionContinue
|
||||
return types.ActionContinue, ""
|
||||
} else {
|
||||
ctx.SetContext(ToolCallsCount, count)
|
||||
}
|
||||
|
||||
//没得到最终答案
|
||||
regexAction := regexp.MustCompile(ActionPattern)
|
||||
regexActionInput := regexp.MustCompile(ActionInputPattern)
|
||||
|
||||
action := regexAction.FindStringSubmatch(content)
|
||||
actionInput := regexActionInput.FindStringSubmatch(content)
|
||||
var url string
|
||||
var headers [][2]string
|
||||
var apiClient wrapper.HttpClient
|
||||
var method string
|
||||
var reqBody []byte
|
||||
var key string
|
||||
var maxExecutionTime int64
|
||||
|
||||
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, apisParam := range config.APIsParam {
|
||||
maxExecutionTime = apisParam.MaxExecutionTime
|
||||
for _, tools_param := range apisParam.ToolsParam {
|
||||
if action == tools_param.ToolName {
|
||||
log.Infof("calls %s", tools_param.ToolName)
|
||||
log.Infof("actionInput: %s", actionInput)
|
||||
|
||||
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
|
||||
//将大模型需要的参数反序列化
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(actionInput), &data); err != nil {
|
||||
log.Debugf("Error: %s", err.Error())
|
||||
return types.ActionContinue, ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
method = tools_param.Method
|
||||
|
||||
// 组装 headers 和 key
|
||||
headers = [][2]string{{"Content-Type", "application/json"}}
|
||||
if apisParam.APIKey.Name != "" {
|
||||
if apisParam.APIKey.In == "query" {
|
||||
key = "?" + apisParam.APIKey.Name + "=" + apisParam.APIKey.Value
|
||||
} else if apisParam.APIKey.In == "header" {
|
||||
headers = append(headers, [2]string{"Authorization", apisParam.APIKey.Name + " " + apisParam.APIKey.Value})
|
||||
}
|
||||
}
|
||||
|
||||
// 组装 URL 和请求体
|
||||
url = apisParam.URL + tools_param.Path + key
|
||||
if method == "GET" {
|
||||
queryParams := make([]string, 0, len(tools_param.ParamName))
|
||||
for _, param := range tools_param.ParamName {
|
||||
if value, ok := data[param]; ok {
|
||||
queryParams = append(queryParams, fmt.Sprintf("%s=%v", param, value))
|
||||
}
|
||||
}
|
||||
if len(queryParams) > 0 {
|
||||
url += "&" + strings.Join(queryParams, "&")
|
||||
}
|
||||
} else if method == "POST" {
|
||||
var err error
|
||||
reqBody, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Debugf("Error marshaling JSON: %s", err.Error())
|
||||
return types.ActionContinue, ""
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("url: %s", url)
|
||||
|
||||
apiClient = config.APIClient[i]
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionPause
|
||||
|
||||
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)
|
||||
}, uint32(maxExecutionTime))
|
||||
if err != nil {
|
||||
log.Debugf("tool calls error: %s", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
} else {
|
||||
return types.ActionContinue, ""
|
||||
}
|
||||
|
||||
return types.ActionPause, ""
|
||||
}
|
||||
|
||||
// 从response接收到firstreq的大模型返回
|
||||
@@ -361,11 +411,12 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byt
|
||||
log.Debugf("[onHttpResponseBody] body to json err: %s", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Infof("first content: %s\n", rawResponse.Choices[0].Message.Content)
|
||||
log.Infof("first content: %s", rawResponse.Choices[0].Message.Content)
|
||||
//如果gpt返回的内容不是空的
|
||||
if rawResponse.Choices[0].Message.Content != "" {
|
||||
//进入agent的循环思考,工具调用的过程中
|
||||
return toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
|
||||
retType, _ := toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
|
||||
return retType
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -13,81 +13,157 @@ Parameters:
|
||||
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:
|
||||
Respond to the human as helpfully and accurately as possible. You have access to the following tools:
|
||||
|
||||
%s
|
||||
{{tools_desc}}
|
||||
|
||||
Use the following format:
|
||||
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
|
||||
Valid "action" values: "Final Answer" or {{tool_names}}
|
||||
|
||||
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.
|
||||
Provide only ONE action per $JSON_BLOB, as shown:
|
||||
|
||||
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s
|
||||
```
|
||||
|
||||
Question: %s
|
||||
{
|
||||
"action": $TOOL_NAME,
|
||||
"action_input": $ACTION_INPUT
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Follow this format:
|
||||
|
||||
Question: input question to answer
|
||||
Thought: consider previous and subsequent steps
|
||||
Action:
|
||||
```
|
||||
$JSON_BLOB
|
||||
```
|
||||
Observation: action result
|
||||
... (repeat Thought/Action/Observation N times)
|
||||
Thought: I know what to respond
|
||||
Action:
|
||||
```
|
||||
|
||||
{
|
||||
"action": "Final Answer",
|
||||
"action_input": "Final response to human"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation:.
|
||||
{{historic_messages}}
|
||||
Question: {{query}}
|
||||
*/
|
||||
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:
|
||||
Respond to the human as helpfully and accurately as possible.You have access to the following tools:
|
||||
|
||||
%s
|
||||
|
||||
Use the following format:
|
||||
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
|
||||
Valid "action" values: "Final Answer" or %s
|
||||
|
||||
Provide only ONE action per $JSON_BLOB, as shown:
|
||||
` + "```" + `
|
||||
{
|
||||
"action": $TOOL_NAME,
|
||||
"action_input": $ACTION_INPUT
|
||||
}
|
||||
` + "```" + `
|
||||
Follow this 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
|
||||
Thought: %s
|
||||
Action: ` + "```" + `$JSON_BLOB` + "```" + `
|
||||
|
||||
Observation: %s
|
||||
... (repeat Thought/Action/Observation N times)
|
||||
Thought: %s
|
||||
Action:` + "```" + `
|
||||
{
|
||||
"action": "Final Answer",
|
||||
"action_input": "Final response to human"
|
||||
}
|
||||
` + "```" + `
|
||||
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate.Format is Action:` + "```" + `$JSON_BLOB` + "```" + `then Observation:.
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
`
|
||||
|
||||
/*
|
||||
尽你所能回答以下问题。你可以使用以下工具:
|
||||
尽可能帮助和准确地回答人的问题。您可以使用以下工具:
|
||||
|
||||
%s
|
||||
{tool_descs}
|
||||
|
||||
请使用以下格式,其中Action字段后必须跟着Action Input字段,并且不要将Action Input替换成Input或者tool等字段,不能出现格式以外的字段名,每个字段在每个轮次只出现一次:
|
||||
Question: 你需要回答的输入问题
|
||||
Thought: 你应该总是思考该做什么
|
||||
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
|
||||
Action Input: 行动的输入,必须出现在Action后。
|
||||
Observation: 行动的结果
|
||||
...(这个Thought/Action/Action Input/Observation可以重复N次)
|
||||
Thought: 我现在知道最终答案
|
||||
Final Answer: 对原始输入问题的最终答案
|
||||
使用 json blob,通过提供 action key(工具名称)和 action_input key(工具输入)来指定工具。
|
||||
有效的 "action"值为 "Final Answer"或 {tool_names}
|
||||
|
||||
再次重申,不要修改以上模板的字段名称,开始吧!
|
||||
每个 $JSON_BLOB 只能提供一个操作,如图所示:
|
||||
|
||||
Question: %s
|
||||
```
|
||||
|
||||
{{
|
||||
"action": $TOOL_NAME,
|
||||
"action_input": $ACTION_INPUT
|
||||
}}
|
||||
|
||||
```
|
||||
|
||||
按照以下格式:
|
||||
Question: 输入要回答的问题
|
||||
Thought: 考虑之前和之后的步骤
|
||||
Action:
|
||||
```
|
||||
$JSON_BLOB
|
||||
```
|
||||
|
||||
Observation: 行动结果
|
||||
...(这个Thought/Action//Observation可以重复N次)
|
||||
Thought: 我知道该回应什么
|
||||
Action:
|
||||
```
|
||||
|
||||
{{
|
||||
"action": "Final Answer",
|
||||
"action_input": "Final response to human"
|
||||
}}
|
||||
|
||||
```
|
||||
|
||||
开始!提醒您始终使用单个操作的有效 json blob 进行响应。必要时使用工具。如果合适,可直接响应。格式为 Action:```$JSON_BLOB```then Observation:.
|
||||
{historic_messages}
|
||||
Question: {input}
|
||||
*/
|
||||
const CH_Template = `
|
||||
尽你所能回答以下问题。你可以使用以下工具:
|
||||
尽可能帮助和准确地回答人的问题。您可以使用以下工具:
|
||||
|
||||
%s
|
||||
|
||||
请使用以下格式,其中Action字段后必须跟着Action Input字段,并且不要将Action Input替换成Input或者tool等字段,不能出现格式以外的字段名,每个字段在每个轮次只出现一次:
|
||||
使用 json blob,通过提供 action key(工具名称)和 action_input key(工具输入)来指定工具。
|
||||
有效的 "action"值为 "Final Answer"或 %s
|
||||
|
||||
每个 $JSON_BLOB 只能提供一个操作,如图所示:
|
||||
` + "```" + `
|
||||
{
|
||||
"action": $TOOL_NAME,
|
||||
"action_input": $ACTION_INPUT
|
||||
}
|
||||
` + "```" + `
|
||||
按照以下格式:
|
||||
Question: %s
|
||||
Thought: %s
|
||||
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
|
||||
Action Input: %s
|
||||
Action: ` + "```" + `$JSON_BLOB` + "```" + `
|
||||
|
||||
Observation: %s
|
||||
...(这个Thought/Action/Action Input/Observation可以重复N次)
|
||||
...(这个Thought/Action//Observation可以重复N次)
|
||||
Thought: %s
|
||||
Final Answer: %s
|
||||
|
||||
Action:` + "```" + `
|
||||
{
|
||||
"action": "Final Answer",
|
||||
"action_input": "Final response to human"
|
||||
}
|
||||
` + "```" + `
|
||||
开始!提醒您始终使用单个操作的有效 json blob 进行响应。必要时使用工具。如果合适,可直接响应。格式为 Action:` + "```" + `$JSON_BLOB` + "```" + `then Observation:.
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# File generated by hgctl. Modify as required.
|
||||
|
||||
docker-compose-test/
|
||||
*
|
||||
|
||||
!/.gitignore
|
||||
|
||||
@@ -1,46 +1,137 @@
|
||||
## 简介
|
||||
---
|
||||
title: AI 缓存
|
||||
keywords: [higress,ai cache]
|
||||
description: AI 缓存插件配置参考
|
||||
---
|
||||
|
||||
**Note**
|
||||
|
||||
> 需要数据面的proxy wasm版本大于等于0.2.100
|
||||
> 编译时,需要带上版本的tag,例如:`tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100" ./`
|
||||
>
|
||||
|
||||
## 功能说明
|
||||
|
||||
LLM 结果缓存插件,默认配置方式可以直接用于 openai 协议的结果缓存,同时支持流式和非流式响应的缓存。
|
||||
|
||||
**提示**
|
||||
|
||||
携带请求头`x-higress-skip-ai-cache: on`时,当前请求将不会使用缓存中的内容,而是直接转发给后端服务,同时也不会缓存该请求返回响应的内容
|
||||
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`认证阶段`
|
||||
插件执行优先级:`10`
|
||||
|
||||
## 配置说明
|
||||
配置分为 3 个部分:向量数据库(vector);文本向量化接口(embedding);缓存数据库(cache),同时也提供了细粒度的 LLM 请求/响应提取参数配置等。
|
||||
|
||||
## 配置说明
|
||||
|
||||
本插件同时支持基于向量数据库的语义化缓存和基于字符串匹配的缓存方法,如果同时配置了向量数据库和缓存数据库,优先使用向量数据库。
|
||||
|
||||
*Note*: 向量数据库(vector) 和 缓存数据库(cache) 不能同时为空,否则本插件无法提供缓存服务。
|
||||
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| vector | string | optional | "" | 向量存储服务提供者类型,例如 dashvector |
|
||||
| embedding | string | optional | "" | 请求文本向量化服务类型,例如 dashscope |
|
||||
| cache | string | optional | "" | 缓存服务类型,例如 redis |
|
||||
| cacheKeyStrategy | string | optional | "lastQuestion" | 决定如何根据历史问题生成缓存键的策略。可选值: "lastQuestion" (使用最后一个问题), "allQuestions" (拼接所有问题) 或 "disabled" (禁用缓存) |
|
||||
| enableSemanticCache | bool | optional | true | 是否启用语义化缓存, 若不启用,则使用字符串匹配的方式来查找缓存,此时需要配置cache服务 |
|
||||
|
||||
根据是否需要启用语义缓存,可以只配置组件的组合为:
|
||||
1. `cache`: 仅启用字符串匹配缓存
|
||||
3. `vector (+ embedding)`: 启用语义化缓存, 其中若 `vector` 未提供字符串表征服务,则需要自行配置 `embedding` 服务
|
||||
2. `vector (+ embedding) + cache`: 启用语义化缓存并用缓存服务存储LLM响应以加速
|
||||
|
||||
注意若不配置相关组件,则可以忽略相应组件的`required`字段。
|
||||
|
||||
|
||||
## 向量数据库服务(vector)
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| vector.type | string | required | "" | 向量存储服务提供者类型,例如 dashvector |
|
||||
| vector.serviceName | string | required | "" | 向量存储服务名称 |
|
||||
| vector.serviceHost | string | required | "" | 向量存储服务域名 |
|
||||
| vector.servicePort | int64 | optional | 443 | 向量存储服务端口 |
|
||||
| vector.apiKey | string | optional | "" | 向量存储服务 API Key |
|
||||
| vector.topK | int | optional | 1 | 返回TopK结果,默认为 1 |
|
||||
| vector.timeout | uint32 | optional | 10000 | 请求向量存储服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
|
||||
| vector.collectionID | string | optional | "" | dashvector 向量存储服务 Collection ID |
|
||||
| vector.threshold | float64 | optional | 1000 | 向量相似度度量阈值 |
|
||||
| vector.thresholdRelation | string | optional | lt | 相似度度量方式有 `Cosine`, `DotProduct`, `Euclidean` 等,前两者值越大相似度越高,后者值越小相似度越高。对于 `Cosine` 和 `DotProduct` 选择 `gt`,对于 `Euclidean` 则选择 `lt`。默认为 `lt`,所有条件包括 `lt` (less than,小于)、`lte` (less than or equal to,小等于)、`gt` (greater than,大于)、`gte` (greater than or equal to,大等于) |
|
||||
|
||||
## 文本向量化服务(embedding)
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| embedding.type | string | required | "" | 请求文本向量化服务类型,例如 dashscope |
|
||||
| embedding.serviceName | string | required | "" | 请求文本向量化服务名称 |
|
||||
| embedding.serviceHost | string | optional | "" | 请求文本向量化服务域名 |
|
||||
| embedding.servicePort | int64 | optional | 443 | 请求文本向量化服务端口 |
|
||||
| embedding.apiKey | string | optional | "" | 请求文本向量化服务的 API Key |
|
||||
| embedding.timeout | uint32 | optional | 10000 | 请求文本向量化服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
|
||||
| embedding.model | string | optional | "" | 请求文本向量化服务的模型名称 |
|
||||
|
||||
|
||||
## 缓存服务(cache)
|
||||
| cache.type | string | required | "" | 缓存服务类型,例如 redis |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| cache.serviceName | string | required | "" | 缓存服务名称 |
|
||||
| cache.serviceHost | string | required | "" | 缓存服务域名 |
|
||||
| cache.servicePort | int64 | optional | 6379 | 缓存服务端口 |
|
||||
| cache.username | string | optional | "" | 缓存服务用户名 |
|
||||
| cache.password | string | optional | "" | 缓存服务密码 |
|
||||
| cache.timeout | uint32 | optional | 10000 | 缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
|
||||
| cache.cacheTTL | int | optional | 0 | 缓存过期时间,单位为秒。默认值是 0,即 永不过期|
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-cache:" | 缓存 Key 的前缀,默认值为 "higress-ai-cache:" |
|
||||
|
||||
|
||||
## 其他配置
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| cacheKeyFrom | string | optional | "messages.@reverse.0.content" | 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| cacheValueFrom | string | optional | "choices.0.message.content" | 从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| cacheStreamValueFrom | string | optional | "choices.0.delta.content" | 从流式响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| cacheToolCallsFrom | string | optional | "choices.0.delta.content.tool_calls" | 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| responseTemplate | string | optional | `{"id":"ai-cache.hit","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}}` | 返回 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
|
||||
| streamResponseTemplate | string | optional | `data:{"id":"ai-cache.hit","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}}\n\ndata:[DONE]\n\n` | 返回流式 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
|
||||
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| cacheKeyFrom.requestBody | string | optional | "messages.@reverse.0.content" | 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| cacheValueFrom.responseBody | string | optional | "choices.0.message.content" | 从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| cacheStreamValueFrom.responseBody | string | optional | "choices.0.delta.content" | 从流式响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-cache:" | Redis缓存Key的前缀 |
|
||||
| cacheTTL | integer | optional | 0 | 缓存的过期时间,单位是秒,默认值为0,即永不过期 |
|
||||
| redis.serviceName | string | requried | - | redis 服务名称,带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | redis 服务端口 |
|
||||
| redis.timeout | integer | optional | 1000 | 请求 redis 的超时时间,单位为毫秒 |
|
||||
| redis.username | string | optional | - | 登陆 redis 的用户名 |
|
||||
| redis.password | string | optional | - | 登陆 redis 的密码 |
|
||||
| returnResponseTemplate | string | optional | `{"id":"from-cache","choices":[%s],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` | 返回 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
|
||||
| returnStreamResponseTemplate | string | optional | `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}}\n\ndata:[DONE]\n\n` | 返回流式 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
|
||||
|
||||
## 配置示例
|
||||
### 基础配置
|
||||
```yaml
|
||||
embedding:
|
||||
type: dashscope
|
||||
serviceName: my_dashscope.dns
|
||||
apiKey: [Your Key]
|
||||
|
||||
vector:
|
||||
type: dashvector
|
||||
serviceName: my_dashvector.dns
|
||||
collectionID: [Your Collection ID]
|
||||
serviceDomain: [Your domain]
|
||||
apiKey: [Your key]
|
||||
|
||||
cache:
|
||||
type: redis
|
||||
serviceName: my_redis.dns
|
||||
servicePort: 6379
|
||||
timeout: 100
|
||||
|
||||
```
|
||||
|
||||
旧版本配置兼容
|
||||
```yaml
|
||||
redis:
|
||||
serviceName: my-redis.dns
|
||||
timeout: 2000
|
||||
serviceName: my_redis.dns
|
||||
servicePort: 6379
|
||||
timeout: 100
|
||||
```
|
||||
|
||||
## 进阶用法
|
||||
|
||||
当前默认的缓存 key 是基于 GJSON PATH 的表达式:`messages.@reverse.0.content` 提取,含义是把 messages 数组反转后取第一项的 content;
|
||||
|
||||
GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key,可以写成: `messages.@reverse.#(role=="user").content`;
|
||||
@@ -50,3 +141,7 @@ GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user
|
||||
还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key,可以写成:`messages.@reverse.#(role=="user")#.content|1`。
|
||||
|
||||
更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. 如果返回的错误为 `error status returned by host: bad argument`,请检查`serviceName`是否正确包含了服务的类型后缀(.dns等)。
|
||||
@@ -6,6 +6,10 @@ description: AI Cache Plugin Configuration Reference
|
||||
## Function Description
|
||||
LLM result caching plugin, the default configuration can be directly used for result caching under the OpenAI protocol, and it supports caching of both streaming and non-streaming responses.
|
||||
|
||||
**Tips**
|
||||
|
||||
When carrying the request header `x-higress-skip-ai-cache: on`, the current request will not use content from the cache but will be directly forwarded to the backend service. Additionally, the response content from this request will not be cached.
|
||||
|
||||
## Runtime Properties
|
||||
Plugin Execution Phase: `Authentication Phase`
|
||||
Plugin Execution Priority: `10`
|
||||
|
||||
135
plugins/wasm-go/extensions/ai-cache/cache/provider.go
vendored
Normal file
135
plugins/wasm-go/extensions/ai-cache/cache/provider.go
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER_TYPE_REDIS = "redis"
|
||||
DEFAULT_CACHE_PREFIX = "higress-ai-cache:"
|
||||
)
|
||||
|
||||
type providerInitializer interface {
|
||||
ValidateConfig(ProviderConfig) error
|
||||
CreateProvider(ProviderConfig) (Provider, error)
|
||||
}
|
||||
|
||||
var (
|
||||
providerInitializers = map[string]providerInitializer{
|
||||
PROVIDER_TYPE_REDIS: &redisProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
type ProviderConfig struct {
|
||||
// @Title zh-CN redis 缓存服务提供者类型
|
||||
// @Description zh-CN 缓存服务提供者类型,例如 redis
|
||||
typ string
|
||||
// @Title zh-CN redis 缓存服务名称
|
||||
// @Description zh-CN 缓存服务名称
|
||||
serviceName string
|
||||
// @Title zh-CN redis 缓存服务端口
|
||||
// @Description zh-CN 缓存服务端口,默认值为6379
|
||||
servicePort int
|
||||
// @Title zh-CN redis 缓存服务地址
|
||||
// @Description zh-CN Cache 缓存服务地址,非必填
|
||||
serviceHost string
|
||||
// @Title zh-CN 缓存服务用户名
|
||||
// @Description zh-CN 缓存服务用户名,非必填
|
||||
username string
|
||||
// @Title zh-CN 缓存服务密码
|
||||
// @Description zh-CN 缓存服务密码,非必填
|
||||
password string
|
||||
// @Title zh-CN 请求超时
|
||||
// @Description zh-CN 请求缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒
|
||||
timeout uint32
|
||||
// @Title zh-CN 缓存过期时间
|
||||
// @Description zh-CN 缓存过期时间,单位为秒。默认值是0,即永不过期
|
||||
cacheTTL int
|
||||
// @Title 缓存 Key 前缀
|
||||
// @Description 缓存 Key 的前缀,默认值为 "higressAiCache:"
|
||||
cacheKeyPrefix string
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetProviderType() string {
|
||||
return c.typ
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.typ = json.Get("type").String()
|
||||
c.serviceName = json.Get("serviceName").String()
|
||||
c.servicePort = int(json.Get("servicePort").Int())
|
||||
if !json.Get("servicePort").Exists() {
|
||||
c.servicePort = 6379
|
||||
}
|
||||
c.serviceHost = json.Get("serviceHost").String()
|
||||
c.username = json.Get("username").String()
|
||||
if !json.Get("username").Exists() {
|
||||
c.username = ""
|
||||
}
|
||||
c.password = json.Get("password").String()
|
||||
if !json.Get("password").Exists() {
|
||||
c.password = ""
|
||||
}
|
||||
c.timeout = uint32(json.Get("timeout").Int())
|
||||
if !json.Get("timeout").Exists() {
|
||||
c.timeout = 10000
|
||||
}
|
||||
c.cacheTTL = int(json.Get("cacheTTL").Int())
|
||||
if !json.Get("cacheTTL").Exists() {
|
||||
c.cacheTTL = 0
|
||||
// c.cacheTTL = 3600000
|
||||
}
|
||||
if json.Get("cacheKeyPrefix").Exists() {
|
||||
c.cacheKeyPrefix = json.Get("cacheKeyPrefix").String()
|
||||
} else {
|
||||
c.cacheKeyPrefix = DEFAULT_CACHE_PREFIX
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) ConvertLegacyJson(json gjson.Result) {
|
||||
c.FromJson(json.Get("redis"))
|
||||
c.typ = "redis"
|
||||
if json.Get("cacheTTL").Exists() {
|
||||
c.cacheTTL = int(json.Get("cacheTTL").Int())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) Validate() error {
|
||||
if c.typ == "" {
|
||||
return errors.New("cache service type is required")
|
||||
}
|
||||
if c.serviceName == "" {
|
||||
return errors.New("cache service name is required")
|
||||
}
|
||||
if c.cacheTTL < 0 {
|
||||
return errors.New("cache TTL must be greater than or equal to 0")
|
||||
}
|
||||
initializer, has := providerInitializers[c.typ]
|
||||
if !has {
|
||||
return errors.New("unknown cache service provider type: " + c.typ)
|
||||
}
|
||||
if err := initializer.ValidateConfig(*c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateProvider(pc ProviderConfig) (Provider, error) {
|
||||
initializer, has := providerInitializers[pc.typ]
|
||||
if !has {
|
||||
return nil, errors.New("unknown provider type: " + pc.typ)
|
||||
}
|
||||
return initializer.CreateProvider(pc)
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
GetProviderType() string
|
||||
Init(username string, password string, timeout uint32) error
|
||||
Get(key string, cb wrapper.RedisResponseCallback) error
|
||||
Set(key string, value string, cb wrapper.RedisResponseCallback) error
|
||||
GetCacheKeyPrefix() string
|
||||
}
|
||||
58
plugins/wasm-go/extensions/ai-cache/cache/redis.go
vendored
Normal file
58
plugins/wasm-go/extensions/ai-cache/cache/redis.go
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type redisProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (r *redisProviderInitializer) ValidateConfig(cf ProviderConfig) error {
|
||||
if len(cf.serviceName) == 0 {
|
||||
return errors.New("cache service name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisProviderInitializer) CreateProvider(cf ProviderConfig) (Provider, error) {
|
||||
rp := redisProvider{
|
||||
config: cf,
|
||||
client: wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: cf.serviceName,
|
||||
Host: cf.serviceHost,
|
||||
Port: int64(cf.servicePort)}),
|
||||
}
|
||||
err := rp.Init(cf.username, cf.password, cf.timeout)
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
type redisProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.RedisClient
|
||||
}
|
||||
|
||||
func (rp *redisProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_REDIS
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Init(username string, password string, timeout uint32) error {
|
||||
return rp.client.Init(rp.config.username, rp.config.password, int64(rp.config.timeout))
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Get(key string, cb wrapper.RedisResponseCallback) error {
|
||||
return rp.client.Get(key, cb)
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Set(key string, value string, cb wrapper.RedisResponseCallback) error {
|
||||
if rp.config.cacheTTL == 0 {
|
||||
return rp.client.Set(key, value, cb)
|
||||
} else {
|
||||
return rp.client.SetEx(key, value, rp.config.cacheTTL, cb)
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *redisProvider) GetCacheKeyPrefix() string {
|
||||
return rp.config.cacheKeyPrefix
|
||||
}
|
||||
225
plugins/wasm-go/extensions/ai-cache/config/config.go
Normal file
225
plugins/wasm-go/extensions/ai-cache/config/config.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/cache"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/embedding"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/vector"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
CACHE_KEY_STRATEGY_LAST_QUESTION = "lastQuestion"
|
||||
CACHE_KEY_STRATEGY_ALL_QUESTIONS = "allQuestions"
|
||||
CACHE_KEY_STRATEGY_DISABLED = "disabled"
|
||||
)
|
||||
|
||||
type PluginConfig struct {
|
||||
// @Title zh-CN 返回 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
ResponseTemplate string
|
||||
// @Title zh-CN 返回流式 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
StreamResponseTemplate string
|
||||
|
||||
cacheProvider cache.Provider
|
||||
embeddingProvider embedding.Provider
|
||||
vectorProvider vector.Provider
|
||||
|
||||
embeddingProviderConfig embedding.ProviderConfig
|
||||
vectorProviderConfig vector.ProviderConfig
|
||||
cacheProviderConfig cache.ProviderConfig
|
||||
|
||||
CacheKeyFrom string
|
||||
CacheValueFrom string
|
||||
CacheStreamValueFrom string
|
||||
CacheToolCallsFrom string
|
||||
|
||||
// @Title zh-CN 启用语义化缓存
|
||||
// @Description zh-CN 控制是否启用语义化缓存功能。true 表示启用,false 表示禁用。
|
||||
EnableSemanticCache bool
|
||||
|
||||
// @Title zh-CN 缓存键策略
|
||||
// @Description zh-CN 决定如何生成缓存键的策略。可选值: "lastQuestion" (使用最后一个问题), "allQuestions" (拼接所有问题) 或 "disabled" (禁用缓存)
|
||||
CacheKeyStrategy string
|
||||
}
|
||||
|
||||
func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
|
||||
c.vectorProviderConfig.FromJson(json.Get("vector"))
|
||||
c.embeddingProviderConfig.FromJson(json.Get("embedding"))
|
||||
c.cacheProviderConfig.FromJson(json.Get("cache"))
|
||||
if json.Get("redis").Exists() {
|
||||
// compatible with legacy config
|
||||
c.cacheProviderConfig.ConvertLegacyJson(json)
|
||||
}
|
||||
|
||||
c.CacheKeyStrategy = json.Get("cacheKeyStrategy").String()
|
||||
if c.CacheKeyStrategy == "" {
|
||||
c.CacheKeyStrategy = CACHE_KEY_STRATEGY_LAST_QUESTION // set default value
|
||||
}
|
||||
c.CacheKeyFrom = json.Get("cacheKeyFrom").String()
|
||||
if c.CacheKeyFrom == "" {
|
||||
c.CacheKeyFrom = "messages.@reverse.0.content"
|
||||
}
|
||||
c.CacheValueFrom = json.Get("cacheValueFrom").String()
|
||||
if c.CacheValueFrom == "" {
|
||||
c.CacheValueFrom = "choices.0.message.content"
|
||||
}
|
||||
c.CacheStreamValueFrom = json.Get("cacheStreamValueFrom").String()
|
||||
if c.CacheStreamValueFrom == "" {
|
||||
c.CacheStreamValueFrom = "choices.0.delta.content"
|
||||
}
|
||||
c.CacheToolCallsFrom = json.Get("cacheToolCallsFrom").String()
|
||||
if c.CacheToolCallsFrom == "" {
|
||||
c.CacheToolCallsFrom = "choices.0.delta.content.tool_calls"
|
||||
}
|
||||
|
||||
c.StreamResponseTemplate = json.Get("streamResponseTemplate").String()
|
||||
if c.StreamResponseTemplate == "" {
|
||||
c.StreamResponseTemplate = `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}}` + "\n\ndata:[DONE]\n\n"
|
||||
}
|
||||
c.ResponseTemplate = json.Get("responseTemplate").String()
|
||||
if c.ResponseTemplate == "" {
|
||||
c.ResponseTemplate = `{"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}}`
|
||||
}
|
||||
|
||||
if json.Get("enableSemanticCache").Exists() {
|
||||
c.EnableSemanticCache = json.Get("enableSemanticCache").Bool()
|
||||
} else {
|
||||
c.EnableSemanticCache = true // set default value to true
|
||||
}
|
||||
|
||||
// compatible with legacy config
|
||||
convertLegacyMapFields(c, json, log)
|
||||
}
|
||||
|
||||
func (c *PluginConfig) Validate() error {
|
||||
// if cache provider is configured, validate it
|
||||
if c.cacheProviderConfig.GetProviderType() != "" {
|
||||
if err := c.cacheProviderConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.embeddingProviderConfig.GetProviderType() != "" {
|
||||
if err := c.embeddingProviderConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.vectorProviderConfig.GetProviderType() != "" {
|
||||
if err := c.vectorProviderConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// cache, vector, and embedding cannot all be empty
|
||||
if c.vectorProviderConfig.GetProviderType() == "" &&
|
||||
c.embeddingProviderConfig.GetProviderType() == "" &&
|
||||
c.cacheProviderConfig.GetProviderType() == "" {
|
||||
return fmt.Errorf("vector, embedding and cache provider cannot be all empty")
|
||||
}
|
||||
|
||||
// Validate the value of CacheKeyStrategy
|
||||
if c.CacheKeyStrategy != CACHE_KEY_STRATEGY_LAST_QUESTION &&
|
||||
c.CacheKeyStrategy != CACHE_KEY_STRATEGY_ALL_QUESTIONS &&
|
||||
c.CacheKeyStrategy != CACHE_KEY_STRATEGY_DISABLED {
|
||||
return fmt.Errorf("invalid CacheKeyStrategy: %s", c.CacheKeyStrategy)
|
||||
}
|
||||
|
||||
// If semantic cache is enabled, ensure necessary components are configured
|
||||
// if c.EnableSemanticCache {
|
||||
// if c.embeddingProviderConfig.GetProviderType() == "" {
|
||||
// return fmt.Errorf("semantic cache is enabled but embedding provider is not configured")
|
||||
// }
|
||||
// // if only configure cache, just warn the user
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
var err error
|
||||
if c.embeddingProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("embedding provider is set to %s", c.embeddingProviderConfig.GetProviderType())
|
||||
c.embeddingProvider, err = embedding.CreateProvider(c.embeddingProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("embedding provider is not configured")
|
||||
c.embeddingProvider = nil
|
||||
}
|
||||
if c.cacheProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("cache provider is set to %s", c.cacheProviderConfig.GetProviderType())
|
||||
c.cacheProvider, err = cache.CreateProvider(c.cacheProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("cache provider is not configured")
|
||||
c.cacheProvider = nil
|
||||
}
|
||||
if c.vectorProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("vector provider is set to %s", c.vectorProviderConfig.GetProviderType())
|
||||
c.vectorProvider, err = vector.CreateProvider(c.vectorProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("vector provider is not configured")
|
||||
c.vectorProvider = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetEmbeddingProvider() embedding.Provider {
|
||||
return c.embeddingProvider
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetVectorProvider() vector.Provider {
|
||||
return c.vectorProvider
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetVectorProviderConfig() vector.ProviderConfig {
|
||||
return c.vectorProviderConfig
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetCacheProvider() cache.Provider {
|
||||
return c.cacheProvider
|
||||
}
|
||||
|
||||
func convertLegacyMapFields(c *PluginConfig, json gjson.Result, log wrapper.Log) {
|
||||
keyMap := map[string]string{
|
||||
"cacheKeyFrom.requestBody": "cacheKeyFrom",
|
||||
"cacheValueFrom.requestBody": "cacheValueFrom",
|
||||
"cacheStreamValueFrom.requestBody": "cacheStreamValueFrom",
|
||||
"returnResponseTemplate": "responseTemplate",
|
||||
"returnStreamResponseTemplate": "streamResponseTemplate",
|
||||
}
|
||||
|
||||
for oldKey, newKey := range keyMap {
|
||||
if json.Get(oldKey).Exists() {
|
||||
log.Debugf("[convertLegacyMapFields] mapping %s to %s", oldKey, newKey)
|
||||
setField(c, newKey, json.Get(oldKey).String(), log)
|
||||
} else {
|
||||
log.Debugf("[convertLegacyMapFields] %s not exists", oldKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setField(c *PluginConfig, fieldName string, value string, log wrapper.Log) {
|
||||
switch fieldName {
|
||||
case "cacheKeyFrom":
|
||||
c.CacheKeyFrom = value
|
||||
case "cacheValueFrom":
|
||||
c.CacheValueFrom = value
|
||||
case "cacheStreamValueFrom":
|
||||
c.CacheStreamValueFrom = value
|
||||
case "responseTemplate":
|
||||
c.ResponseTemplate = value
|
||||
case "streamResponseTemplate":
|
||||
c.StreamResponseTemplate = value
|
||||
}
|
||||
log.Debugf("[setField] set %s to %s", fieldName, value)
|
||||
}
|
||||
275
plugins/wasm-go/extensions/ai-cache/core.go
Normal file
275
plugins/wasm-go/extensions/ai-cache/core.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/vector"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tidwall/resp"
|
||||
)
|
||||
|
||||
// CheckCacheForKey checks if the key is in the cache, or triggers similarity search if not found.
|
||||
func CheckCacheForKey(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool, useSimilaritySearch bool) error {
|
||||
activeCacheProvider := c.GetCacheProvider()
|
||||
if activeCacheProvider == nil {
|
||||
log.Debugf("[%s] [CheckCacheForKey] no cache provider configured, performing similarity search", PLUGIN_NAME)
|
||||
return performSimilaritySearch(key, ctx, c, log, key, stream)
|
||||
}
|
||||
|
||||
queryKey := activeCacheProvider.GetCacheKeyPrefix() + key
|
||||
log.Debugf("[%s] [CheckCacheForKey] querying cache with key: %s", PLUGIN_NAME, queryKey)
|
||||
|
||||
err := activeCacheProvider.Get(queryKey, func(response resp.Value) {
|
||||
handleCacheResponse(key, response, ctx, log, stream, c, useSimilaritySearch)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("[%s] [CheckCacheForKey] failed to retrieve key: %s from cache, error: %v", PLUGIN_NAME, key, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCacheResponse processes cache response and handles cache hits and misses.
|
||||
func handleCacheResponse(key string, response resp.Value, ctx wrapper.HttpContext, log wrapper.Log, stream bool, c config.PluginConfig, useSimilaritySearch bool) {
|
||||
if err := response.Error(); err == nil && !response.IsNull() {
|
||||
log.Infof("[%s] cache hit for key: %s", PLUGIN_NAME, key)
|
||||
processCacheHit(key, response.String(), stream, ctx, c, log)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("[%s] [handleCacheResponse] cache miss for key: %s", PLUGIN_NAME, key)
|
||||
if err := response.Error(); err != nil {
|
||||
log.Errorf("[%s] [handleCacheResponse] error retrieving key: %s from cache, error: %v", PLUGIN_NAME, key, err)
|
||||
}
|
||||
|
||||
if useSimilaritySearch && c.EnableSemanticCache {
|
||||
if err := performSimilaritySearch(key, ctx, c, log, key, stream); err != nil {
|
||||
log.Errorf("[%s] [handleCacheResponse] failed to perform similarity search for key: %s, error: %v", PLUGIN_NAME, key, err)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
} else {
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
|
||||
// processCacheHit handles a successful cache hit.
|
||||
func processCacheHit(key string, response string, stream bool, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) {
|
||||
if strings.TrimSpace(response) == "" {
|
||||
log.Warnf("[%s] [processCacheHit] cached response for key %s is empty", PLUGIN_NAME, key)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[%s] [processCacheHit] cached response for key %s: %s", PLUGIN_NAME, key, response)
|
||||
|
||||
// Escape the response to ensure consistent formatting
|
||||
escapedResponse := strings.Trim(strconv.Quote(response), "\"")
|
||||
|
||||
ctx.SetContext(CACHE_KEY_CONTEXT_KEY, nil)
|
||||
|
||||
if stream {
|
||||
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(c.StreamResponseTemplate, escapedResponse)), -1)
|
||||
} else {
|
||||
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(c.ResponseTemplate, escapedResponse)), -1)
|
||||
}
|
||||
}
|
||||
|
||||
// performSimilaritySearch determines the appropriate similarity search method to use.
|
||||
func performSimilaritySearch(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, queryString string, stream bool) error {
|
||||
activeVectorProvider := c.GetVectorProvider()
|
||||
if activeVectorProvider == nil {
|
||||
return logAndReturnError(log, "[performSimilaritySearch] no vector provider configured for similarity search")
|
||||
}
|
||||
|
||||
// Check if the active vector provider implements the StringQuerier interface.
|
||||
if _, ok := activeVectorProvider.(vector.StringQuerier); ok {
|
||||
log.Debugf("[%s] [performSimilaritySearch] active vector provider implements StringQuerier interface, performing string query", PLUGIN_NAME)
|
||||
return performStringQuery(key, queryString, ctx, c, log, stream)
|
||||
}
|
||||
|
||||
// Check if the active vector provider implements the EmbeddingQuerier interface.
|
||||
if _, ok := activeVectorProvider.(vector.EmbeddingQuerier); ok {
|
||||
log.Debugf("[%s] [performSimilaritySearch] active vector provider implements EmbeddingQuerier interface, performing embedding query", PLUGIN_NAME)
|
||||
return performEmbeddingQuery(key, ctx, c, log, stream)
|
||||
}
|
||||
|
||||
return logAndReturnError(log, "[performSimilaritySearch] no suitable querier or embedding provider available for similarity search")
|
||||
}
|
||||
|
||||
// performStringQuery executes the string-based similarity search.
|
||||
func performStringQuery(key string, queryString string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool) error {
|
||||
stringQuerier, ok := c.GetVectorProvider().(vector.StringQuerier)
|
||||
if !ok {
|
||||
return logAndReturnError(log, "[performStringQuery] active vector provider does not implement StringQuerier interface")
|
||||
}
|
||||
|
||||
return stringQuerier.QueryString(queryString, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error) {
|
||||
handleQueryResults(key, results, ctx, log, stream, c, err)
|
||||
})
|
||||
}
|
||||
|
||||
// performEmbeddingQuery executes the embedding-based similarity search.
|
||||
func performEmbeddingQuery(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool) error {
|
||||
embeddingQuerier, ok := c.GetVectorProvider().(vector.EmbeddingQuerier)
|
||||
if !ok {
|
||||
return logAndReturnError(log, fmt.Sprintf("[performEmbeddingQuery] active vector provider does not implement EmbeddingQuerier interface"))
|
||||
}
|
||||
|
||||
activeEmbeddingProvider := c.GetEmbeddingProvider()
|
||||
if activeEmbeddingProvider == nil {
|
||||
return logAndReturnError(log, fmt.Sprintf("[performEmbeddingQuery] no embedding provider configured for similarity search"))
|
||||
}
|
||||
|
||||
return activeEmbeddingProvider.GetEmbedding(key, ctx, log, func(textEmbedding []float64, err error) {
|
||||
log.Debugf("[%s] [performEmbeddingQuery] GetEmbedding success, length of embedding: %d, error: %v", PLUGIN_NAME, len(textEmbedding), err)
|
||||
if err != nil {
|
||||
handleInternalError(err, fmt.Sprintf("[%s] [performEmbeddingQuery] error getting embedding for key: %s", PLUGIN_NAME, key), log)
|
||||
return
|
||||
}
|
||||
ctx.SetContext(CACHE_KEY_EMBEDDING_KEY, textEmbedding)
|
||||
|
||||
err = embeddingQuerier.QueryEmbedding(textEmbedding, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error) {
|
||||
handleQueryResults(key, results, ctx, log, stream, c, err)
|
||||
})
|
||||
if err != nil {
|
||||
handleInternalError(err, fmt.Sprintf("[%s] [performEmbeddingQuery] error querying vector database for key: %s", PLUGIN_NAME, key), log)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// handleQueryResults processes the results of similarity search and determines next actions.
|
||||
func handleQueryResults(key string, results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, stream bool, c config.PluginConfig, err error) {
|
||||
if err != nil {
|
||||
handleInternalError(err, fmt.Sprintf("[%s] [handleQueryResults] error querying vector database for key: %s", PLUGIN_NAME, key), log)
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
log.Warnf("[%s] [handleQueryResults] no similar keys found for key: %s", PLUGIN_NAME, key)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
|
||||
mostSimilarData := results[0]
|
||||
log.Debugf("[%s] [handleQueryResults] for key: %s, the most similar key found: %s with score: %f", PLUGIN_NAME, key, mostSimilarData.Text, mostSimilarData.Score)
|
||||
simThreshold := c.GetVectorProviderConfig().Threshold
|
||||
simThresholdRelation := c.GetVectorProviderConfig().ThresholdRelation
|
||||
if compare(simThresholdRelation, mostSimilarData.Score, simThreshold) {
|
||||
log.Infof("[%s] key accepted: %s with score: %f", PLUGIN_NAME, mostSimilarData.Text, mostSimilarData.Score)
|
||||
if mostSimilarData.Answer != "" {
|
||||
// direct return the answer if available
|
||||
cacheResponse(ctx, c, key, mostSimilarData.Answer, log)
|
||||
processCacheHit(key, mostSimilarData.Answer, stream, ctx, c, log)
|
||||
} else {
|
||||
if c.GetCacheProvider() != nil {
|
||||
CheckCacheForKey(mostSimilarData.Text, ctx, c, log, stream, false)
|
||||
} else {
|
||||
// Otherwise, do not check the cache, directly return
|
||||
log.Infof("[%s] cache hit for key: %s, but no corresponding answer found in the vector database", PLUGIN_NAME, mostSimilarData.Text)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Infof("[%s] score not meet the threshold %f: %s with score %f", PLUGIN_NAME, simThreshold, mostSimilarData.Text, mostSimilarData.Score)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
|
||||
// logAndReturnError logs an error and returns it.
|
||||
func logAndReturnError(log wrapper.Log, message string) error {
|
||||
message = fmt.Sprintf("[%s] %s", PLUGIN_NAME, message)
|
||||
log.Errorf(message)
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
// handleInternalError logs an error and resumes the HTTP request.
|
||||
func handleInternalError(err error, message string, log wrapper.Log) {
|
||||
if err != nil {
|
||||
log.Errorf("[%s] [handleInternalError] %s: %v", PLUGIN_NAME, message, err)
|
||||
} else {
|
||||
log.Errorf("[%s] [handleInternalError] %s", PLUGIN_NAME, message)
|
||||
}
|
||||
// proxywasm.SendHttpResponse(500, [][2]string{{"content-type", "text/plain"}}, []byte("Internal Server Error"), -1)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
|
||||
// Caches the response value
|
||||
func cacheResponse(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log wrapper.Log) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
log.Warnf("[%s] [cacheResponse] cached value for key %s is empty", PLUGIN_NAME, key)
|
||||
return
|
||||
}
|
||||
|
||||
activeCacheProvider := c.GetCacheProvider()
|
||||
if activeCacheProvider != nil {
|
||||
queryKey := activeCacheProvider.GetCacheKeyPrefix() + key
|
||||
_ = activeCacheProvider.Set(queryKey, value, nil)
|
||||
log.Debugf("[%s] [cacheResponse] cache set success, key: %s, length of value: %d", PLUGIN_NAME, queryKey, len(value))
|
||||
}
|
||||
}
|
||||
|
||||
// Handles embedding upload if available
|
||||
func uploadEmbeddingAndAnswer(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log wrapper.Log) {
|
||||
embedding := ctx.GetContext(CACHE_KEY_EMBEDDING_KEY)
|
||||
if embedding == nil {
|
||||
return
|
||||
}
|
||||
|
||||
emb, ok := embedding.([]float64)
|
||||
if !ok {
|
||||
log.Errorf("[%s] [uploadEmbeddingAndAnswer] embedding is not of expected type []float64", PLUGIN_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
activeVectorProvider := c.GetVectorProvider()
|
||||
if activeVectorProvider == nil {
|
||||
log.Debugf("[%s] [uploadEmbeddingAndAnswer] no vector provider configured for uploading embedding", PLUGIN_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to upload answer embedding first
|
||||
if ansEmbUploader, ok := activeVectorProvider.(vector.AnswerAndEmbeddingUploader); ok {
|
||||
log.Infof("[%s] uploading answer embedding for key: %s", PLUGIN_NAME, key)
|
||||
err := ansEmbUploader.UploadAnswerAndEmbedding(key, emb, value, ctx, log, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[%s] [uploadEmbeddingAndAnswer] failed to upload answer embedding for key: %s, error: %v", PLUGIN_NAME, key, err)
|
||||
} else {
|
||||
return // If successful, return early
|
||||
}
|
||||
}
|
||||
|
||||
// If answer embedding upload fails, attempt normal embedding upload
|
||||
if embUploader, ok := activeVectorProvider.(vector.EmbeddingUploader); ok {
|
||||
log.Infof("[%s] uploading embedding for key: %s", PLUGIN_NAME, key)
|
||||
err := embUploader.UploadEmbedding(key, emb, ctx, log, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[%s] [uploadEmbeddingAndAnswer] failed to upload embedding for key: %s, error: %v", PLUGIN_NAME, key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主要用于相似度/距离/点积判断
|
||||
// 余弦相似度度量的是两个向量在方向上的相似程度。相似度越高,两个向量越接近。
|
||||
// 距离度量的是两个向量在空间上的远近程度。距离越小,两个向量越接近。
|
||||
// compare 函数根据操作符进行判断并返回结果
|
||||
func compare(operator string, value1 float64, value2 float64) bool {
|
||||
switch operator {
|
||||
case "gt":
|
||||
return value1 > value2
|
||||
case "gte":
|
||||
return value1 >= value2
|
||||
case "lt":
|
||||
return value1 < value2
|
||||
case "lte":
|
||||
return value1 <= value2
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
187
plugins/wasm-go/extensions/ai-cache/embedding/dashscope.go
Normal file
187
plugins/wasm-go/extensions/ai-cache/embedding/dashscope.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
const (
|
||||
DASHSCOPE_DOMAIN = "dashscope.aliyuncs.com"
|
||||
DASHSCOPE_PORT = 443
|
||||
DASHSCOPE_DEFAULT_MODEL_NAME = "text-embedding-v2"
|
||||
DASHSCOPE_ENDPOINT = "/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||
)
|
||||
|
||||
type dashScopeProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (d *dashScopeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiKey == "" {
|
||||
return errors.New("[DashScope] apiKey is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dashScopeProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
|
||||
if c.servicePort == 0 {
|
||||
c.servicePort = DASHSCOPE_PORT
|
||||
}
|
||||
if c.serviceHost == "" {
|
||||
c.serviceHost = DASHSCOPE_DOMAIN
|
||||
}
|
||||
return &DSProvider{
|
||||
config: c,
|
||||
client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.serviceName,
|
||||
Host: c.serviceHost,
|
||||
Port: int64(c.servicePort),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DSProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_DASHSCOPE
|
||||
}
|
||||
|
||||
type Embedding struct {
|
||||
Embedding []float64 `json:"embedding"`
|
||||
TextIndex int `json:"text_index"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Texts []string `json:"texts"`
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
TextType string `json:"text_type"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Output Output `json:"output"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Embeddings []Embedding `json:"embeddings"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type EmbeddingRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input Input `json:"input"`
|
||||
Parameters Params `json:"parameters"`
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
Vector []float64 `json:"vector"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
}
|
||||
|
||||
type DSProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.HttpClient
|
||||
}
|
||||
|
||||
func (d *DSProvider) constructParameters(texts []string, log wrapper.Log) (string, [][2]string, []byte, error) {
|
||||
|
||||
model := d.config.model
|
||||
|
||||
if model == "" {
|
||||
model = DASHSCOPE_DEFAULT_MODEL_NAME
|
||||
}
|
||||
data := EmbeddingRequest{
|
||||
Model: model,
|
||||
Input: Input{
|
||||
Texts: texts,
|
||||
},
|
||||
Parameters: Params{
|
||||
TextType: "query",
|
||||
},
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal request data: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
if d.config.apiKey == "" {
|
||||
err := errors.New("dashScopeKey is empty")
|
||||
log.Errorf("failed to construct headers: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"Authorization", "Bearer " + d.config.apiKey},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
|
||||
return DASHSCOPE_ENDPOINT, headers, requestBody, err
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ID string `json:"id"`
|
||||
Vector []float64 `json:"vector,omitempty"`
|
||||
Fields map[string]interface{} `json:"fields"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
func (d *DSProvider) parseTextEmbedding(responseBody []byte) (*Response, error) {
|
||||
var resp Response
|
||||
err := json.Unmarshal(responseBody, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *DSProvider) GetEmbedding(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(emb []float64, err error)) error {
|
||||
embUrl, embHeaders, embRequestBody, err := d.constructParameters([]string{queryString}, log)
|
||||
if err != nil {
|
||||
log.Errorf("failed to construct parameters: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var resp *Response
|
||||
err = d.client.Post(embUrl, embHeaders, embRequestBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
err = errors.New("failed to get embedding due to status code: " + strconv.Itoa(statusCode))
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
|
||||
|
||||
resp, err = d.parseTextEmbedding(responseBody)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse response: %v", err)
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Output.Embeddings) == 0 {
|
||||
err = errors.New("no embedding found in response")
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
callback(resp.Output.Embeddings[0].Embedding, nil)
|
||||
|
||||
}, d.config.timeout)
|
||||
return err
|
||||
}
|
||||
112
plugins/wasm-go/extensions/ai-cache/embedding/provider.go
Normal file
112
plugins/wasm-go/extensions/ai-cache/embedding/provider.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER_TYPE_DASHSCOPE = "dashscope"
|
||||
PROVIDER_TYPE_TEXTIN = "textin"
|
||||
)
|
||||
|
||||
type providerInitializer interface {
|
||||
ValidateConfig(ProviderConfig) error
|
||||
CreateProvider(ProviderConfig) (Provider, error)
|
||||
}
|
||||
|
||||
var (
|
||||
providerInitializers = map[string]providerInitializer{
|
||||
PROVIDER_TYPE_DASHSCOPE: &dashScopeProviderInitializer{},
|
||||
PROVIDER_TYPE_TEXTIN: &textInProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
type ProviderConfig struct {
|
||||
// @Title zh-CN 文本特征提取服务提供者类型
|
||||
// @Description zh-CN 文本特征提取服务提供者类型,例如 DashScope
|
||||
typ string
|
||||
// @Title zh-CN DashScope 文本特征提取服务名称
|
||||
// @Description zh-CN 文本特征提取服务名称
|
||||
serviceName string
|
||||
// @Title zh-CN 文本特征提取服务域名
|
||||
// @Description zh-CN 文本特征提取服务域名
|
||||
serviceHost string
|
||||
// @Title zh-CN 文本特征提取服务端口
|
||||
// @Description zh-CN 文本特征提取服务端口
|
||||
servicePort int64
|
||||
// @Title zh-CN 文本特征提取服务 API Key
|
||||
// @Description zh-CN 文本特征提取服务 API Key
|
||||
apiKey string
|
||||
//@Title zh-CN TextIn x-ti-app-id
|
||||
// @Description zh-CN 仅适用于 TextIn 服务。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinAppId string
|
||||
//@Title zh-CN TextIn x-ti-secret-code
|
||||
// @Description zh-CN 仅适用于 TextIn 服务。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinSecretCode string
|
||||
//@Title zh-CN TextIn request matryoshka_dim
|
||||
// @Description zh-CN 仅适用于 TextIn 服务, 指定返回的向量维度。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinMatryoshkaDim int
|
||||
// @Title zh-CN 文本特征提取服务超时时间
|
||||
// @Description zh-CN 文本特征提取服务超时时间
|
||||
timeout uint32
|
||||
// @Title zh-CN 文本特征提取服务使用的模型
|
||||
// @Description zh-CN 用于文本特征提取的模型名称, 在 DashScope 中默认为 "text-embedding-v1"
|
||||
model string
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.typ = json.Get("type").String()
|
||||
c.serviceName = json.Get("serviceName").String()
|
||||
c.serviceHost = json.Get("serviceHost").String()
|
||||
c.servicePort = json.Get("servicePort").Int()
|
||||
c.apiKey = json.Get("apiKey").String()
|
||||
c.textinAppId = json.Get("textinAppId").String()
|
||||
c.textinSecretCode = json.Get("textinSecretCode").String()
|
||||
c.textinMatryoshkaDim = int(json.Get("textinMatryoshkaDim").Int())
|
||||
c.timeout = uint32(json.Get("timeout").Int())
|
||||
c.model = json.Get("model").String()
|
||||
if c.timeout == 0 {
|
||||
c.timeout = 10000
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) Validate() error {
|
||||
if c.serviceName == "" {
|
||||
return errors.New("embedding service name is required")
|
||||
}
|
||||
if c.typ == "" {
|
||||
return errors.New("embedding service type is required")
|
||||
}
|
||||
initializer, has := providerInitializers[c.typ]
|
||||
if !has {
|
||||
return errors.New("unknown embedding service provider type: " + c.typ)
|
||||
}
|
||||
if err := initializer.ValidateConfig(*c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetProviderType() string {
|
||||
return c.typ
|
||||
}
|
||||
|
||||
func CreateProvider(pc ProviderConfig) (Provider, error) {
|
||||
initializer, has := providerInitializers[pc.typ]
|
||||
if !has {
|
||||
return nil, errors.New("unknown provider type: " + pc.typ)
|
||||
}
|
||||
return initializer.CreateProvider(pc)
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
GetProviderType() string
|
||||
GetEmbedding(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(emb []float64, err error)) error
|
||||
}
|
||||
161
plugins/wasm-go/extensions/ai-cache/embedding/textin.go
Normal file
161
plugins/wasm-go/extensions/ai-cache/embedding/textin.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
const (
|
||||
TEXTIN_DOMAIN = "api.textin.com"
|
||||
TEXTIN_PORT = 443
|
||||
TEXTIN_DEFAULT_MODEL_NAME = "acge-text-embedding"
|
||||
TEXTIN_ENDPOINT = "/ai/service/v1/acge_embedding"
|
||||
)
|
||||
|
||||
type textInProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (t *textInProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.textinAppId == "" {
|
||||
return errors.New("embedding service TextIn App ID is required")
|
||||
}
|
||||
if config.textinSecretCode == "" {
|
||||
return errors.New("embedding service TextIn Secret Code is required")
|
||||
}
|
||||
if config.textinMatryoshkaDim == 0 {
|
||||
return errors.New("embedding service TextIn Matryoshka Dim is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *textInProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
|
||||
if c.servicePort == 0 {
|
||||
c.servicePort = TEXTIN_PORT
|
||||
}
|
||||
if c.serviceHost == "" {
|
||||
c.serviceHost = TEXTIN_DOMAIN
|
||||
}
|
||||
return &TIProvider{
|
||||
config: c,
|
||||
client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.serviceName,
|
||||
Host: c.serviceHost,
|
||||
Port: int64(c.servicePort),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TIProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_TEXTIN
|
||||
}
|
||||
|
||||
type TextInResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Duration float64 `json:"duration"`
|
||||
Result TextInResult `json:"result"`
|
||||
}
|
||||
|
||||
type TextInResult struct {
|
||||
Embeddings [][]float64 `json:"embedding"`
|
||||
MatryoshkaDim int `json:"matryoshka_dim"`
|
||||
}
|
||||
|
||||
type TextInEmbeddingRequest struct {
|
||||
Input []string `json:"input"`
|
||||
MatryoshkaDim int `json:"matryoshka_dim"`
|
||||
}
|
||||
|
||||
type TIProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.HttpClient
|
||||
}
|
||||
|
||||
func (t *TIProvider) constructParameters(texts []string, log wrapper.Log) (string, [][2]string, []byte, error) {
|
||||
|
||||
data := TextInEmbeddingRequest{
|
||||
Input: texts,
|
||||
MatryoshkaDim: t.config.textinMatryoshkaDim,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal request data: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
if t.config.textinAppId == "" {
|
||||
err := errors.New("textinAppId is empty")
|
||||
log.Errorf("failed to construct headers: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if t.config.textinSecretCode == "" {
|
||||
err := errors.New("textinSecretCode is empty")
|
||||
log.Errorf("failed to construct headers: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"x-ti-app-id", t.config.textinAppId},
|
||||
{"x-ti-secret-code", t.config.textinSecretCode},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
|
||||
return TEXTIN_ENDPOINT, headers, requestBody, err
|
||||
}
|
||||
|
||||
func (t *TIProvider) parseTextEmbedding(responseBody []byte) (*TextInResponse, error) {
|
||||
var resp TextInResponse
|
||||
err := json.Unmarshal(responseBody, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (t *TIProvider) GetEmbedding(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(emb []float64, err error)) error {
|
||||
embUrl, embHeaders, embRequestBody, err := t.constructParameters([]string{queryString}, log)
|
||||
if err != nil {
|
||||
log.Errorf("failed to construct parameters: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var resp *TextInResponse
|
||||
err = t.client.Post(embUrl, embHeaders, embRequestBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
err = errors.New("failed to get embedding due to status code: " + strconv.Itoa(statusCode))
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
|
||||
|
||||
resp, err = t.parseTextEmbedding(responseBody)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse response: %v", err)
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Result.Embeddings) == 0 {
|
||||
err = errors.New("no embedding found in response")
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
callback(resp.Result.Embeddings[0], nil)
|
||||
|
||||
}, t.config.timeout)
|
||||
return err
|
||||
}
|
||||
27
plugins/wasm-go/extensions/ai-cache/embedding/weaviate.go
Normal file
27
plugins/wasm-go/extensions/ai-cache/embedding/weaviate.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package embedding
|
||||
|
||||
// import (
|
||||
// "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
// )
|
||||
|
||||
// const (
|
||||
// weaviateURL = "172.17.0.1:8081"
|
||||
// )
|
||||
|
||||
// type weaviateProviderInitializer struct {
|
||||
// }
|
||||
|
||||
// func (d *weaviateProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func (d *weaviateProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
// return &DSProvider{
|
||||
// config: config,
|
||||
// client: wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
// ServiceName: config.ServiceName,
|
||||
// Port: dashScopePort,
|
||||
// Domain: dashScopeDomain,
|
||||
// }),
|
||||
// }, nil
|
||||
// }
|
||||
@@ -7,17 +7,18 @@ go 1.19
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240528060522-53bccf89f441
|
||||
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.14.3
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/tidwall/resp v0.1.1
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
// github.com/weaviate/weaviate-go-client/v4 v4.15.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.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-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 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.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/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=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
// File generated by hgctl. Modify as required.
|
||||
// See: https://higress.io/zh-cn/docs/user/wasm-go#2-%E7%BC%96%E5%86%99-maingo-%E6%96%87%E4%BB%B6
|
||||
|
||||
// 这个文件中主要将OnHttpRequestHeaders、OnHttpRequestBody、OnHttpResponseHeaders、OnHttpResponseBody这四个函数实现
|
||||
// 其中的缓存思路调用cache.go中的逻辑,然后cache.go中的逻辑会调用textEmbeddingProvider和vectorStoreProvider中的逻辑(实例)
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
|
||||
"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"
|
||||
"github.com/tidwall/resp"
|
||||
)
|
||||
|
||||
const (
|
||||
CacheKeyContextKey = "cacheKey"
|
||||
CacheContentContextKey = "cacheContent"
|
||||
PartialMessageContextKey = "partialMessage"
|
||||
ToolCallsContextKey = "toolCalls"
|
||||
StreamContextKey = "stream"
|
||||
DefaultCacheKeyPrefix = "higress-ai-cache:"
|
||||
PLUGIN_NAME = "ai-cache"
|
||||
CACHE_KEY_CONTEXT_KEY = "cacheKey"
|
||||
CACHE_KEY_EMBEDDING_KEY = "cacheKeyEmbedding"
|
||||
CACHE_CONTENT_CONTEXT_KEY = "cacheContent"
|
||||
PARTIAL_MESSAGE_CONTEXT_KEY = "partialMessage"
|
||||
TOOL_CALLS_CONTEXT_KEY = "toolCalls"
|
||||
STREAM_CONTEXT_KEY = "stream"
|
||||
SKIP_CACHE_HEADER = "x-higress-skip-ai-cache"
|
||||
ERROR_PARTIAL_MESSAGE_KEY = "errorPartialMessage"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// CreateClient()
|
||||
wrapper.SetCtx(
|
||||
"ai-cache",
|
||||
PLUGIN_NAME,
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
@@ -35,337 +36,152 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
// @Name ai-cache
|
||||
// @Category protocol
|
||||
// @Phase AUTHN
|
||||
// @Priority 10
|
||||
// @Title zh-CN AI Cache
|
||||
// @Description zh-CN 大模型结果缓存
|
||||
// @IconUrl
|
||||
// @Version 0.1.0
|
||||
//
|
||||
// @Contact.name johnlanni
|
||||
// @Contact.url
|
||||
// @Contact.email
|
||||
//
|
||||
// @Example
|
||||
// redis:
|
||||
// serviceName: my-redis.dns
|
||||
// timeout: 2000
|
||||
// cacheKeyFrom:
|
||||
// requestBody: "messages.@reverse.0.content"
|
||||
// cacheValueFrom:
|
||||
// responseBody: "choices.0.message.content"
|
||||
// cacheStreamValueFrom:
|
||||
// responseBody: "choices.0.delta.content"
|
||||
// returnResponseTemplate: |
|
||||
// {"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 {
|
||||
// @Title zh-CN redis 服务名称
|
||||
// @Description zh-CN 带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local
|
||||
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
|
||||
// @Title zh-CN redis 服务端口
|
||||
// @Description zh-CN 默认值为6379
|
||||
ServicePort int `required:"false" yaml:"servicePort" json:"servicePort"`
|
||||
// @Title zh-CN 用户名
|
||||
// @Description zh-CN 登陆 redis 的用户名,非必填
|
||||
Username string `required:"false" yaml:"username" json:"username"`
|
||||
// @Title zh-CN 密码
|
||||
// @Description zh-CN 登陆 redis 的密码,非必填,可以只填密码
|
||||
Password string `required:"false" yaml:"password" json:"password"`
|
||||
// @Title zh-CN 请求超时
|
||||
// @Description zh-CN 请求 redis 的超时时间,单位为毫秒。默认值是1000,即1秒
|
||||
Timeout int `required:"false" yaml:"timeout" json:"timeout"`
|
||||
func parseConfig(json gjson.Result, c *config.PluginConfig, log wrapper.Log) error {
|
||||
// config.EmbeddingProviderConfig.FromJson(json.Get("embeddingProvider"))
|
||||
// config.VectorDatabaseProviderConfig.FromJson(json.Get("vectorBaseProvider"))
|
||||
// config.RedisConfig.FromJson(json.Get("redis"))
|
||||
c.FromJson(json, log)
|
||||
if err := c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Note that initializing the client during the parseConfig phase may cause errors, such as Redis not being usable in Docker Compose.
|
||||
if err := c.Complete(log); err != nil {
|
||||
log.Errorf("complete config failed: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type KVExtractor struct {
|
||||
// @Title zh-CN 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
|
||||
RequestBody string `required:"false" yaml:"requestBody" json:"requestBody"`
|
||||
// @Title zh-CN 从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
|
||||
ResponseBody string `required:"false" yaml:"responseBody" json:"responseBody"`
|
||||
}
|
||||
|
||||
type PluginConfig struct {
|
||||
// @Title zh-CN Redis 地址信息
|
||||
// @Description zh-CN 用于存储缓存结果的 Redis 地址
|
||||
RedisInfo RedisInfo `required:"true" yaml:"redis" json:"redis"`
|
||||
// @Title zh-CN 缓存 key 的来源
|
||||
// @Description zh-CN 往 redis 里存时,使用的 key 的提取方式
|
||||
CacheKeyFrom KVExtractor `required:"true" yaml:"cacheKeyFrom" json:"cacheKeyFrom"`
|
||||
// @Title zh-CN 缓存 value 的来源
|
||||
// @Description zh-CN 往 redis 里存时,使用的 value 的提取方式
|
||||
CacheValueFrom KVExtractor `required:"true" yaml:"cacheValueFrom" json:"cacheValueFrom"`
|
||||
// @Title zh-CN 流式响应下,缓存 value 的来源
|
||||
// @Description zh-CN 往 redis 里存时,使用的 value 的提取方式
|
||||
CacheStreamValueFrom KVExtractor `required:"true" yaml:"cacheStreamValueFrom" json:"cacheStreamValueFrom"`
|
||||
// @Title zh-CN 返回 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
ReturnResponseTemplate string `required:"true" yaml:"returnResponseTemplate" json:"returnResponseTemplate"`
|
||||
// @Title zh-CN 返回流式 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
ReturnStreamResponseTemplate string `required:"true" yaml:"returnStreamResponseTemplate" json:"returnStreamResponseTemplate"`
|
||||
// @Title zh-CN 缓存的过期时间
|
||||
// @Description zh-CN 单位是秒,默认值为0,即永不过期
|
||||
CacheTTL int `required:"false" yaml:"cacheTTL" json:"cacheTTL"`
|
||||
// @Title zh-CN Redis缓存Key的前缀
|
||||
// @Description zh-CN 默认值是"higress-ai-cache:"
|
||||
CacheKeyPrefix string `required:"false" yaml:"cacheKeyPrefix" json:"cacheKeyPrefix"`
|
||||
redisClient wrapper.RedisClient `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
c.RedisInfo.ServiceName = json.Get("redis.serviceName").String()
|
||||
if c.RedisInfo.ServiceName == "" {
|
||||
return errors.New("redis service name must not by empty")
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
|
||||
skipCache, _ := proxywasm.GetHttpRequestHeader(SKIP_CACHE_HEADER)
|
||||
if skipCache == "on" {
|
||||
ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
c.RedisInfo.ServicePort = int(json.Get("redis.servicePort").Int())
|
||||
if c.RedisInfo.ServicePort == 0 {
|
||||
if strings.HasSuffix(c.RedisInfo.ServiceName, ".static") {
|
||||
// use default logic port which is 80 for static service
|
||||
c.RedisInfo.ServicePort = 80
|
||||
} else {
|
||||
c.RedisInfo.ServicePort = 6379
|
||||
}
|
||||
}
|
||||
c.RedisInfo.Username = json.Get("redis.username").String()
|
||||
c.RedisInfo.Password = json.Get("redis.password").String()
|
||||
c.RedisInfo.Timeout = int(json.Get("redis.timeout").Int())
|
||||
if c.RedisInfo.Timeout == 0 {
|
||||
c.RedisInfo.Timeout = 1000
|
||||
}
|
||||
c.CacheKeyFrom.RequestBody = json.Get("cacheKeyFrom.requestBody").String()
|
||||
if c.CacheKeyFrom.RequestBody == "" {
|
||||
c.CacheKeyFrom.RequestBody = "messages.@reverse.0.content"
|
||||
}
|
||||
c.CacheValueFrom.ResponseBody = json.Get("cacheValueFrom.responseBody").String()
|
||||
if c.CacheValueFrom.ResponseBody == "" {
|
||||
c.CacheValueFrom.ResponseBody = "choices.0.message.content"
|
||||
}
|
||||
c.CacheStreamValueFrom.ResponseBody = json.Get("cacheStreamValueFrom.responseBody").String()
|
||||
if c.CacheStreamValueFrom.ResponseBody == "" {
|
||||
c.CacheStreamValueFrom.ResponseBody = "choices.0.delta.content"
|
||||
}
|
||||
c.ReturnResponseTemplate = json.Get("returnResponseTemplate").String()
|
||||
if c.ReturnResponseTemplate == "" {
|
||||
c.ReturnResponseTemplate = `{"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}}`
|
||||
}
|
||||
c.ReturnStreamResponseTemplate = json.Get("returnStreamResponseTemplate").String()
|
||||
if c.ReturnStreamResponseTemplate == "" {
|
||||
c.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}}` + "\n\ndata:[DONE]\n\n"
|
||||
}
|
||||
c.CacheKeyPrefix = json.Get("cacheKeyPrefix").String()
|
||||
if c.CacheKeyPrefix == "" {
|
||||
c.CacheKeyPrefix = DefaultCacheKeyPrefix
|
||||
}
|
||||
c.redisClient = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.RedisInfo.ServiceName,
|
||||
Port: int64(c.RedisInfo.ServicePort),
|
||||
})
|
||||
return c.redisClient.Init(c.RedisInfo.Username, c.RedisInfo.Password, int64(c.RedisInfo.Timeout))
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
contentType, _ := proxywasm.GetHttpRequestHeader("content-type")
|
||||
// The request does not have a body.
|
||||
if contentType == "" {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if !strings.Contains(contentType, "application/json") {
|
||||
log.Warnf("content is not json, can't process:%s", contentType)
|
||||
log.Warnf("content is not json, can't process: %s", contentType)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = 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.
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
|
||||
func TrimQuote(source string) string {
|
||||
return strings.Trim(source, `"`)
|
||||
}
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
// TODO: It may be necessary to support stream mode determination for different LLM providers.
|
||||
stream := false
|
||||
if bodyJson.Get("stream").Bool() {
|
||||
stream = true
|
||||
ctx.SetContext(StreamContextKey, struct{}{})
|
||||
} else if ctx.GetContext(StreamContextKey) != nil {
|
||||
stream = true
|
||||
ctx.SetContext(STREAM_CONTEXT_KEY, struct{}{})
|
||||
}
|
||||
key := TrimQuote(bodyJson.Get(config.CacheKeyFrom.RequestBody).Raw)
|
||||
|
||||
var key string
|
||||
if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_LAST_QUESTION {
|
||||
log.Debugf("[onHttpRequestBody] cache key strategy is last question, cache key from: %s", c.CacheKeyFrom)
|
||||
key = bodyJson.Get(c.CacheKeyFrom).String()
|
||||
} else if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_ALL_QUESTIONS {
|
||||
log.Debugf("[onHttpRequestBody] cache key strategy is all questions, cache key from: messages")
|
||||
messages := bodyJson.Get("messages").Array()
|
||||
var userMessages []string
|
||||
for _, msg := range messages {
|
||||
if msg.Get("role").String() == "user" {
|
||||
userMessages = append(userMessages, msg.Get("content").String())
|
||||
}
|
||||
}
|
||||
key = strings.Join(userMessages, "\n")
|
||||
} else if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_DISABLED {
|
||||
log.Info("[onHttpRequestBody] cache key strategy is disabled")
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
log.Warnf("[onHttpRequestBody] unknown cache key strategy: %s", c.CacheKeyStrategy)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
ctx.SetContext(CACHE_KEY_CONTEXT_KEY, key)
|
||||
log.Debugf("[onHttpRequestBody] key: %s", key)
|
||||
if key == "" {
|
||||
log.Debug("parse key from request body failed")
|
||||
log.Debug("[onHttpRequestBody] parse key from request body failed")
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.SetContext(CacheKeyContextKey, key)
|
||||
err := config.redisClient.Get(config.CacheKeyPrefix+key, func(response resp.Value) {
|
||||
if err := response.Error(); err != nil {
|
||||
log.Errorf("redis get key:%s failed, err:%v", key, err)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
if response.IsNull() {
|
||||
log.Debugf("cache miss, key:%s", key)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
log.Debugf("cache hit, key:%s", key)
|
||||
ctx.SetContext(CacheKeyContextKey, nil)
|
||||
if !stream {
|
||||
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.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 {
|
||||
log.Error("redis access failed")
|
||||
|
||||
if err := CheckCacheForKey(key, ctx, c, log, stream, true); err != nil {
|
||||
log.Errorf("[onHttpRequestBody] check cache for key: %s failed, error: %v", key, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage string, log wrapper.Log) string {
|
||||
subMessages := strings.Split(sseMessage, "\n")
|
||||
var message string
|
||||
for _, msg := range subMessages {
|
||||
if strings.HasPrefix(msg, "data:") {
|
||||
message = msg
|
||||
break
|
||||
}
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
|
||||
skipCache := ctx.GetContext(SKIP_CACHE_HEADER)
|
||||
if skipCache != nil {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
if len(message) < 6 {
|
||||
log.Errorf("invalid message:%s", message)
|
||||
return ""
|
||||
}
|
||||
// skip the prefix "data:"
|
||||
bodyJson := message[5:]
|
||||
if gjson.Get(bodyJson, config.CacheStreamValueFrom.ResponseBody).Exists() {
|
||||
tempContentI := ctx.GetContext(CacheContentContextKey)
|
||||
if tempContentI == nil {
|
||||
content := TrimQuote(gjson.Get(bodyJson, config.CacheStreamValueFrom.ResponseBody).Raw)
|
||||
ctx.SetContext(CacheContentContextKey, content)
|
||||
return content
|
||||
}
|
||||
append := TrimQuote(gjson.Get(bodyJson, config.CacheStreamValueFrom.ResponseBody).Raw)
|
||||
content := tempContentI.(string) + append
|
||||
ctx.SetContext(CacheContentContextKey, content)
|
||||
return content
|
||||
} else if gjson.Get(bodyJson, "choices.0.delta.content.tool_calls").Exists() {
|
||||
// TODO: compatible with other providers
|
||||
ctx.SetContext(ToolCallsContextKey, struct{}{})
|
||||
return ""
|
||||
}
|
||||
log.Debugf("unknown message:%s", bodyJson)
|
||||
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{}{})
|
||||
ctx.SetContext(STREAM_CONTEXT_KEY, struct{}{})
|
||||
}
|
||||
|
||||
if ctx.GetContext(ERROR_PARTIAL_MESSAGE_KEY) != nil {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
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
|
||||
return chunk
|
||||
}
|
||||
keyI := ctx.GetContext(CacheKeyContextKey)
|
||||
if keyI == nil {
|
||||
return chunk
|
||||
}
|
||||
if !isLastChunk {
|
||||
stream := ctx.GetContext(StreamContextKey)
|
||||
if stream == nil {
|
||||
tempContentI := ctx.GetContext(CacheContentContextKey)
|
||||
if tempContentI == nil {
|
||||
ctx.SetContext(CacheContentContextKey, chunk)
|
||||
return chunk
|
||||
}
|
||||
tempContent := tempContentI.([]byte)
|
||||
tempContent = append(tempContent, chunk...)
|
||||
ctx.SetContext(CacheContentContextKey, tempContent)
|
||||
} else {
|
||||
var partialMessage []byte
|
||||
partialMessageI := ctx.GetContext(PartialMessageContextKey)
|
||||
if partialMessageI != nil {
|
||||
partialMessage = append(partialMessageI.([]byte), chunk...)
|
||||
} else {
|
||||
partialMessage = chunk
|
||||
}
|
||||
messages := strings.Split(string(partialMessage), "\n\n")
|
||||
for i, msg := range messages {
|
||||
if i < len(messages)-1 {
|
||||
// process complete message
|
||||
processSSEMessage(ctx, config, msg, log)
|
||||
}
|
||||
}
|
||||
if !strings.HasSuffix(string(partialMessage), "\n\n") {
|
||||
ctx.SetContext(PartialMessageContextKey, []byte(messages[len(messages)-1]))
|
||||
} else {
|
||||
ctx.SetContext(PartialMessageContextKey, nil)
|
||||
}
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
// last chunk
|
||||
key := keyI.(string)
|
||||
stream := ctx.GetContext(StreamContextKey)
|
||||
var value string
|
||||
if stream == nil {
|
||||
var body []byte
|
||||
tempContentI := ctx.GetContext(CacheContentContextKey)
|
||||
if tempContentI != nil {
|
||||
body = append(tempContentI.([]byte), chunk...)
|
||||
} else {
|
||||
body = chunk
|
||||
}
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
|
||||
log.Debugf("[onHttpResponseBody] is last chunk: %v", isLastChunk)
|
||||
log.Debugf("[onHttpResponseBody] chunk: %s", string(chunk))
|
||||
|
||||
value = TrimQuote(bodyJson.Get(config.CacheValueFrom.ResponseBody).Raw)
|
||||
if value == "" {
|
||||
log.Warnf("parse value from response body failded, body:%s", body)
|
||||
return chunk
|
||||
if ctx.GetContext(TOOL_CALLS_CONTEXT_KEY) != nil {
|
||||
return chunk
|
||||
}
|
||||
|
||||
key := ctx.GetContext(CACHE_KEY_CONTEXT_KEY)
|
||||
if key == nil {
|
||||
log.Debug("[onHttpResponseBody] key is nil, skip cache")
|
||||
return chunk
|
||||
}
|
||||
|
||||
if !isLastChunk {
|
||||
if err := handleNonLastChunk(ctx, c, chunk, log); err != nil {
|
||||
log.Errorf("[onHttpResponseBody] handle non last chunk failed, error: %v", err)
|
||||
// Set an empty struct in the context to indicate an error in processing the partial message
|
||||
ctx.SetContext(ERROR_PARTIAL_MESSAGE_KEY, struct{}{})
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
|
||||
stream := ctx.GetContext(STREAM_CONTEXT_KEY)
|
||||
var value string
|
||||
var err error
|
||||
if stream == nil {
|
||||
value, err = processNonStreamLastChunk(ctx, c, chunk, log)
|
||||
} else {
|
||||
if len(chunk) > 0 {
|
||||
var lastMessage []byte
|
||||
partialMessageI := ctx.GetContext(PartialMessageContextKey)
|
||||
if partialMessageI != nil {
|
||||
lastMessage = append(partialMessageI.([]byte), chunk...)
|
||||
} else {
|
||||
lastMessage = chunk
|
||||
}
|
||||
if !strings.HasSuffix(string(lastMessage), "\n\n") {
|
||||
log.Warnf("invalid lastMessage:%s", lastMessage)
|
||||
return chunk
|
||||
}
|
||||
// remove the last \n\n
|
||||
lastMessage = lastMessage[:len(lastMessage)-2]
|
||||
value = processSSEMessage(ctx, config, string(lastMessage), log)
|
||||
} else {
|
||||
tempContentI := ctx.GetContext(CacheContentContextKey)
|
||||
if tempContentI == nil {
|
||||
return chunk
|
||||
}
|
||||
value = tempContentI.(string)
|
||||
}
|
||||
value, err = processStreamLastChunk(ctx, c, chunk, log)
|
||||
}
|
||||
config.redisClient.Set(config.CacheKeyPrefix+key, value, nil)
|
||||
if config.CacheTTL != 0 {
|
||||
config.redisClient.Expire(config.CacheKeyPrefix+key, config.CacheTTL, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("[onHttpResponseBody] process last chunk failed, error: %v", err)
|
||||
return chunk
|
||||
}
|
||||
|
||||
cacheResponse(ctx, c, key.(string), value, log)
|
||||
uploadEmbeddingAndAnswer(ctx, c, key.(string), value, log)
|
||||
return chunk
|
||||
}
|
||||
|
||||
155
plugins/wasm-go/extensions/ai-cache/util.go
Normal file
155
plugins/wasm-go/extensions/ai-cache/util.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func handleNonLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
stream := ctx.GetContext(STREAM_CONTEXT_KEY)
|
||||
err := error(nil)
|
||||
if stream == nil {
|
||||
err = handleNonStreamChunk(ctx, c, chunk, log)
|
||||
} else {
|
||||
err = handleStreamChunk(ctx, c, chunk, log)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func handleNonStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
if tempContentI == nil {
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, chunk)
|
||||
return nil
|
||||
}
|
||||
tempContent := tempContentI.([]byte)
|
||||
tempContent = append(tempContent, chunk...)
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, tempContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
var partialMessage []byte
|
||||
partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
|
||||
log.Debugf("[handleStreamChunk] cache content: %v", ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY))
|
||||
if partialMessageI != nil {
|
||||
partialMessage = append(partialMessageI.([]byte), chunk...)
|
||||
} else {
|
||||
partialMessage = chunk
|
||||
}
|
||||
messages := strings.Split(string(partialMessage), "\n\n")
|
||||
for i, msg := range messages {
|
||||
if i < len(messages)-1 {
|
||||
_, err := processSSEMessage(ctx, c, msg, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[handleStreamChunk] processSSEMessage failed, error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !strings.HasSuffix(string(partialMessage), "\n\n") {
|
||||
ctx.SetContext(PARTIAL_MESSAGE_CONTEXT_KEY, []byte(messages[len(messages)-1]))
|
||||
} else {
|
||||
ctx.SetContext(PARTIAL_MESSAGE_CONTEXT_KEY, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processNonStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) (string, error) {
|
||||
var body []byte
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
if tempContentI != nil {
|
||||
body = append(tempContentI.([]byte), chunk...)
|
||||
} else {
|
||||
body = chunk
|
||||
}
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
value := bodyJson.Get(c.CacheValueFrom).String()
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", fmt.Errorf("[processNonStreamLastChunk] parse value from response body failed, body:%s", body)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func processStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) (string, error) {
|
||||
if len(chunk) > 0 {
|
||||
var lastMessage []byte
|
||||
partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
|
||||
if partialMessageI != nil {
|
||||
lastMessage = append(partialMessageI.([]byte), chunk...)
|
||||
} else {
|
||||
lastMessage = chunk
|
||||
}
|
||||
if !strings.HasSuffix(string(lastMessage), "\n\n") {
|
||||
return "", fmt.Errorf("[processStreamLastChunk] invalid lastMessage:%s", lastMessage)
|
||||
}
|
||||
lastMessage = lastMessage[:len(lastMessage)-2]
|
||||
value, err := processSSEMessage(ctx, c, string(lastMessage), log)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("[processStreamLastChunk] processSSEMessage failed, error: %v", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
if tempContentI == nil {
|
||||
return "", nil
|
||||
}
|
||||
return tempContentI.(string), nil
|
||||
}
|
||||
|
||||
func processSSEMessage(ctx wrapper.HttpContext, c config.PluginConfig, sseMessage string, log wrapper.Log) (string, error) {
|
||||
subMessages := strings.Split(sseMessage, "\n")
|
||||
var message string
|
||||
for _, msg := range subMessages {
|
||||
if strings.HasPrefix(msg, "data:") {
|
||||
message = msg
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(message) < 6 {
|
||||
return "", fmt.Errorf("[processSSEMessage] invalid message: %s", message)
|
||||
}
|
||||
|
||||
// skip the prefix "data:"
|
||||
bodyJson := message[5:]
|
||||
|
||||
if strings.TrimSpace(bodyJson) == "[DONE]" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Extract values from JSON fields
|
||||
responseBody := gjson.Get(bodyJson, c.CacheStreamValueFrom)
|
||||
toolCalls := gjson.Get(bodyJson, c.CacheToolCallsFrom)
|
||||
|
||||
if toolCalls.Exists() {
|
||||
// TODO: Temporarily store the tool_calls value in the context for processing
|
||||
ctx.SetContext(TOOL_CALLS_CONTEXT_KEY, toolCalls.String())
|
||||
}
|
||||
|
||||
// Check if the ResponseBody field exists
|
||||
if !responseBody.Exists() {
|
||||
if ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY) != nil {
|
||||
log.Debugf("[processSSEMessage] unable to extract content from message; cache content is not nil: %s", message)
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("[processSSEMessage] unable to extract content from message; cache content is nil: %s", message)
|
||||
} else {
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
|
||||
// If there is no content in the cache, initialize and set the content
|
||||
if tempContentI == nil {
|
||||
content := responseBody.String()
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Update the content in the cache
|
||||
appendMsg := responseBody.String()
|
||||
content := tempContentI.(string) + appendMsg
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
256
plugins/wasm-go/extensions/ai-cache/vector/dashvector.go
Normal file
256
plugins/wasm-go/extensions/ai-cache/vector/dashvector.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package vector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type dashVectorProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (d *dashVectorProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if len(config.apiKey) == 0 {
|
||||
return errors.New("[DashVector] apiKey is required")
|
||||
}
|
||||
if len(config.collectionID) == 0 {
|
||||
return errors.New("[DashVector] collectionID is required")
|
||||
}
|
||||
if len(config.serviceName) == 0 {
|
||||
return errors.New("[DashVector] serviceName is required")
|
||||
}
|
||||
if len(config.serviceHost) == 0 {
|
||||
return errors.New("[DashVector] serviceHost is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dashVectorProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &DvProvider{
|
||||
config: config,
|
||||
client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: config.serviceName,
|
||||
Host: config.serviceHost,
|
||||
Port: int64(config.servicePort),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DvProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.HttpClient
|
||||
}
|
||||
|
||||
func (d *DvProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_DASH_VECTOR
|
||||
}
|
||||
|
||||
// type embeddingRequest struct {
|
||||
// Model string `json:"model"`
|
||||
// Input input `json:"input"`
|
||||
// Parameters params `json:"parameters"`
|
||||
// }
|
||||
|
||||
// type params struct {
|
||||
// TextType string `json:"text_type"`
|
||||
// }
|
||||
|
||||
// type input struct {
|
||||
// Texts []string `json:"texts"`
|
||||
// }
|
||||
|
||||
// queryResponse 定义查询响应的结构
|
||||
type queryResponse struct {
|
||||
Code int `json:"code"`
|
||||
RequestID string `json:"request_id"`
|
||||
Message string `json:"message"`
|
||||
Output []result `json:"output"`
|
||||
}
|
||||
|
||||
// queryRequest 定义查询请求的结构
|
||||
type queryRequest struct {
|
||||
Vector []float64 `json:"vector"`
|
||||
TopK int `json:"topk"`
|
||||
IncludeVector bool `json:"include_vector"`
|
||||
}
|
||||
|
||||
// result 定义查询结果的结构
|
||||
type result struct {
|
||||
ID string `json:"id"`
|
||||
Vector []float64 `json:"vector,omitempty"` // omitempty 使得如果 vector 是空,它将不会被序列化
|
||||
Fields map[string]interface{} `json:"fields"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
func (d *DvProvider) constructEmbeddingQueryParameters(vector []float64) (string, []byte, [][2]string, error) {
|
||||
url := fmt.Sprintf("/v1/collections/%s/query", d.config.collectionID)
|
||||
|
||||
requestData := queryRequest{
|
||||
Vector: vector,
|
||||
TopK: d.config.topK,
|
||||
IncludeVector: false,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(requestData)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
header := [][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{"dashvector-auth-token", d.config.apiKey},
|
||||
}
|
||||
|
||||
return url, requestBody, header, nil
|
||||
}
|
||||
|
||||
func (d *DvProvider) parseQueryResponse(responseBody []byte) (queryResponse, error) {
|
||||
var queryResp queryResponse
|
||||
err := json.Unmarshal(responseBody, &queryResp)
|
||||
if err != nil {
|
||||
return queryResponse{}, err
|
||||
}
|
||||
return queryResp, nil
|
||||
}
|
||||
|
||||
func (d *DvProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
url, body, headers, err := d.constructEmbeddingQueryParameters(emb)
|
||||
log.Debugf("url:%s, body:%s, headers:%v", url, string(body), headers)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to construct embedding query parameters: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.client.Post(url, headers, body,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
err = nil
|
||||
if statusCode != http.StatusOK {
|
||||
err = fmt.Errorf("failed to query embedding: %d", statusCode)
|
||||
callback(nil, ctx, log, err)
|
||||
return
|
||||
}
|
||||
log.Debugf("query embedding response: %d, %s", statusCode, responseBody)
|
||||
results, err := d.ParseQueryResponse(responseBody, ctx, log)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse query response: %v", err)
|
||||
}
|
||||
callback(results, ctx, log, err)
|
||||
},
|
||||
d.config.timeout)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to query embedding: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getStringValue(fields map[string]interface{}, key string) string {
|
||||
if val, ok := fields[key]; ok {
|
||||
return val.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *DvProvider) ParseQueryResponse(responseBody []byte, ctx wrapper.HttpContext, log wrapper.Log) ([]QueryResult, error) {
|
||||
resp, err := d.parseQueryResponse(responseBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Output) == 0 {
|
||||
return nil, errors.New("no query results found in response")
|
||||
}
|
||||
|
||||
results := make([]QueryResult, 0, len(resp.Output))
|
||||
|
||||
for _, output := range resp.Output {
|
||||
result := QueryResult{
|
||||
Text: getStringValue(output.Fields, "query"),
|
||||
Embedding: output.Vector,
|
||||
Score: output.Score,
|
||||
Answer: getStringValue(output.Fields, "answer"),
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type document struct {
|
||||
Vector []float64 `json:"vector"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
}
|
||||
|
||||
type insertRequest struct {
|
||||
Docs []document `json:"docs"`
|
||||
}
|
||||
|
||||
func (d *DvProvider) constructUploadParameters(emb []float64, queryString string, answer string) (string, []byte, [][2]string, error) {
|
||||
url := "/v1/collections/" + d.config.collectionID + "/docs"
|
||||
|
||||
doc := document{
|
||||
Vector: emb,
|
||||
Fields: map[string]string{
|
||||
"query": queryString,
|
||||
"answer": answer,
|
||||
},
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(insertRequest{Docs: []document{doc}})
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
header := [][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{"dashvector-auth-token", d.config.apiKey},
|
||||
}
|
||||
|
||||
return url, requestBody, header, err
|
||||
}
|
||||
|
||||
func (d *DvProvider) UploadEmbedding(queryString string, queryEmb []float64, ctx wrapper.HttpContext, log wrapper.Log, callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
url, body, headers, err := d.constructUploadParameters(queryEmb, queryString, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.client.Post(
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
log.Debugf("statusCode:%d, responseBody:%s", statusCode, string(responseBody))
|
||||
if statusCode != http.StatusOK {
|
||||
err = fmt.Errorf("failed to upload embedding: %d", statusCode)
|
||||
}
|
||||
callback(ctx, log, err)
|
||||
},
|
||||
d.config.timeout)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DvProvider) UploadAnswerAndEmbedding(queryString string, queryEmb []float64, queryAnswer string, ctx wrapper.HttpContext, log wrapper.Log, callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
url, body, headers, err := d.constructUploadParameters(queryEmb, queryString, queryAnswer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.client.Post(
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
log.Debugf("statusCode:%d, responseBody:%s", statusCode, string(responseBody))
|
||||
if statusCode != http.StatusOK {
|
||||
err = fmt.Errorf("failed to upload embedding: %d", statusCode)
|
||||
}
|
||||
callback(ctx, log, err)
|
||||
},
|
||||
d.config.timeout)
|
||||
return err
|
||||
}
|
||||
167
plugins/wasm-go/extensions/ai-cache/vector/provider.go
Normal file
167
plugins/wasm-go/extensions/ai-cache/vector/provider.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package vector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER_TYPE_DASH_VECTOR = "dashvector"
|
||||
PROVIDER_TYPE_CHROMA = "chroma"
|
||||
)
|
||||
|
||||
type providerInitializer interface {
|
||||
ValidateConfig(ProviderConfig) error
|
||||
CreateProvider(ProviderConfig) (Provider, error)
|
||||
}
|
||||
|
||||
var (
|
||||
providerInitializers = map[string]providerInitializer{
|
||||
PROVIDER_TYPE_DASH_VECTOR: &dashVectorProviderInitializer{},
|
||||
// PROVIDER_TYPE_CHROMA: &chromaProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
// QueryResult 定义通用的查询结果的结构体
|
||||
type QueryResult struct {
|
||||
Text string // 相似的文本
|
||||
Embedding []float64 // 相似文本的向量
|
||||
Score float64 // 文本的向量相似度或距离等度量
|
||||
Answer string // 相似文本对应的LLM生成的回答
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
GetProviderType() string
|
||||
}
|
||||
|
||||
type EmbeddingQuerier interface {
|
||||
QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
}
|
||||
|
||||
type EmbeddingUploader interface {
|
||||
UploadEmbedding(
|
||||
queryString string,
|
||||
queryEmb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
}
|
||||
|
||||
type AnswerAndEmbeddingUploader interface {
|
||||
UploadAnswerAndEmbedding(
|
||||
queryString string,
|
||||
queryEmb []float64,
|
||||
answer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
}
|
||||
|
||||
type StringQuerier interface {
|
||||
QueryString(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
}
|
||||
|
||||
type SimilarityThresholdProvider interface {
|
||||
GetSimilarityThreshold() float64
|
||||
}
|
||||
|
||||
type ProviderConfig struct {
|
||||
// @Title zh-CN 向量存储服务提供者类型
|
||||
// @Description zh-CN 向量存储服务提供者类型,例如 dashvector、chroma
|
||||
typ string
|
||||
// @Title zh-CN 向量存储服务名称
|
||||
// @Description zh-CN 向量存储服务名称
|
||||
serviceName string
|
||||
// @Title zh-CN 向量存储服务域名
|
||||
// @Description zh-CN 向量存储服务域名
|
||||
serviceHost string
|
||||
// @Title zh-CN 向量存储服务端口
|
||||
// @Description zh-CN 向量存储服务端口
|
||||
servicePort int64
|
||||
// @Title zh-CN 向量存储服务 API Key
|
||||
// @Description zh-CN 向量存储服务 API Key
|
||||
apiKey string
|
||||
// @Title zh-CN 返回TopK结果
|
||||
// @Description zh-CN 返回TopK结果,默认为 1
|
||||
topK int
|
||||
// @Title zh-CN 请求超时
|
||||
// @Description zh-CN 请求向量存储服务的超时时间,单位为毫秒。默认值是10000,即10秒
|
||||
timeout uint32
|
||||
// @Title zh-CN DashVector 向量存储服务 Collection ID
|
||||
// @Description zh-CN DashVector 向量存储服务 Collection ID
|
||||
collectionID string
|
||||
// @Title zh-CN 相似度度量阈值
|
||||
// @Description zh-CN 默认相似度度量阈值,默认为 1000。
|
||||
Threshold float64
|
||||
// @Title zh-CN 相似度度量比较方式
|
||||
// @Description zh-CN 相似度度量比较方式,默认为小于。
|
||||
// 相似度度量方式有 Cosine, DotProduct, Euclidean 等,前两者值越大相似度越高,后者值越小相似度越高。
|
||||
// 所以需要允许自定义比较方式,对于 Cosine 和 DotProduct 选择 gt,对于 Euclidean 则选择 lt。
|
||||
// 默认为 lt,所有条件包括 lt (less than,小于)、lte (less than or equal to,小等于)、gt (greater than,大于)、gte (greater than or equal to,大等于)
|
||||
ThresholdRelation string
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetProviderType() string {
|
||||
return c.typ
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.typ = json.Get("type").String()
|
||||
// DashVector
|
||||
c.serviceName = json.Get("serviceName").String()
|
||||
c.serviceHost = json.Get("serviceHost").String()
|
||||
c.servicePort = int64(json.Get("servicePort").Int())
|
||||
if c.servicePort == 0 {
|
||||
c.servicePort = 443
|
||||
}
|
||||
c.apiKey = json.Get("apiKey").String()
|
||||
c.collectionID = json.Get("collectionID").String()
|
||||
c.topK = int(json.Get("topK").Int())
|
||||
if c.topK == 0 {
|
||||
c.topK = 1
|
||||
}
|
||||
c.timeout = uint32(json.Get("timeout").Int())
|
||||
if c.timeout == 0 {
|
||||
c.timeout = 10000
|
||||
}
|
||||
c.Threshold = json.Get("threshold").Float()
|
||||
if c.Threshold == 0 {
|
||||
c.Threshold = 1000
|
||||
}
|
||||
c.ThresholdRelation = json.Get("thresholdRelation").String()
|
||||
if c.ThresholdRelation == "" {
|
||||
c.ThresholdRelation = "lt"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) Validate() error {
|
||||
if c.typ == "" {
|
||||
return errors.New("vector database service is required")
|
||||
}
|
||||
initializer, has := providerInitializers[c.typ]
|
||||
if !has {
|
||||
return errors.New("unknown vector database service provider type: " + c.typ)
|
||||
}
|
||||
if err := initializer.ValidateConfig(*c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateProvider(pc ProviderConfig) (Provider, error) {
|
||||
initializer, has := providerInitializers[pc.typ]
|
||||
if !has {
|
||||
return nil, errors.New("unknown provider type: " + pc.typ)
|
||||
}
|
||||
return initializer.CreateProvider(pc)
|
||||
}
|
||||
4
plugins/wasm-go/extensions/ai-proxy/Makefile
Normal file
4
plugins/wasm-go/extensions/ai-proxy/Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
.DEFAULT:
|
||||
build:
|
||||
tinygo build -o ai-proxy.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100' ./main.go
|
||||
mv ai-proxy.wasm ../../../../docker-compose-test/
|
||||
@@ -143,6 +143,10 @@ Groq 所对应的 `type` 为 `groq`。它并无特有的配置字段。
|
||||
|
||||
360智脑所对应的 `type` 为 `ai360`。它并无特有的配置字段。
|
||||
|
||||
#### GitHub模型
|
||||
|
||||
GitHub模型所对应的 `type` 为 `github`。它并无特有的配置字段。
|
||||
|
||||
#### Mistral
|
||||
|
||||
Mistral 所对应的 `type` 为 `mistral`。它并无特有的配置字段。
|
||||
@@ -215,6 +219,10 @@ DeepL 所对应的 `type` 为 `deepl`。它特有的配置字段如下:
|
||||
| ------------ | -------- | -------- | ------ | ---------------------------- |
|
||||
| `targetLang` | string | 必填 | - | DeepL 翻译服务需要的目标语种 |
|
||||
|
||||
#### Cohere
|
||||
|
||||
Cohere 所对应的 `type` 为 `cohere`。它并无特有的配置字段。
|
||||
|
||||
## 用法示例
|
||||
|
||||
### 使用 OpenAI 协议代理 Azure OpenAI 服务
|
||||
@@ -602,6 +610,77 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用original协议代理百炼智能体应用
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "YOUR_DASHSCOPE_API_TOKEN"
|
||||
protocol: original
|
||||
```
|
||||
|
||||
**请求实例**
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"prompt": "介绍一下Dubbo"
|
||||
},
|
||||
"parameters": {},
|
||||
"debug": {}
|
||||
}
|
||||
```
|
||||
|
||||
**响应实例**
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"finish_reason": "stop",
|
||||
"session_id": "677e7e8fbb874e1b84792b65042e1599",
|
||||
"text": "Apache Dubbo 是一个..."
|
||||
},
|
||||
"usage": {
|
||||
"models": [
|
||||
{
|
||||
"output_tokens": 449,
|
||||
"model_id": "qwen-max",
|
||||
"input_tokens": 282
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "b59e45e3-5af4-91df-b7c6-9d746fd3297c"
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理豆包大模型服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: doubao
|
||||
apiTokens:
|
||||
- YOUR_DOUBAO_API_KEY
|
||||
modelMapping:
|
||||
'*': YOUR_DOUBAO_ENDPOINT
|
||||
timeout: 1200000
|
||||
```
|
||||
|
||||
### 使用 original 协议代理 Coze 应用
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: coze
|
||||
apiTokens:
|
||||
- YOUR_COZE_API_KEY
|
||||
protocol: original
|
||||
```
|
||||
|
||||
### 使用月之暗面配合其原生的文件上下文
|
||||
|
||||
提前上传文件至月之暗面,以文件内容作为上下文使用其 AI 服务。
|
||||
@@ -770,6 +849,7 @@ provider:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理混元服务
|
||||
|
||||
**配置信息**
|
||||
@@ -787,9 +867,10 @@ provider:
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
请求脚本:
|
||||
```sh
|
||||
|
||||
请求脚本:
|
||||
|
||||
```shell
|
||||
curl --location 'http://<your higress domain>/v1/chat/completions' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
@@ -953,6 +1034,107 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 GitHub 模型服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: github
|
||||
apiTokens:
|
||||
- "YOUR_GITHUB_ACCESS_TOKEN"
|
||||
modelMapping:
|
||||
"gpt-4o": "gpt-4o"
|
||||
"gpt-4": "Phi-3.5-MoE-instruct"
|
||||
"gpt-3.5": "cohere-command-r-08-2024"
|
||||
"text-embedding-3-large": "text-embedding-3-large"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the capital of France?"
|
||||
}
|
||||
],
|
||||
"stream": true,
|
||||
"temperature": 1.0,
|
||||
"top_p": 1.0,
|
||||
"max_tokens": 1000,
|
||||
"model": "gpt-4o"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"logprobs": null,
|
||||
"message": {
|
||||
"content": "The capital of France is Paris.",
|
||||
"role": "assistant"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1728131051,
|
||||
"id": "chatcmpl-AEy7PU2JImdsD1W6Jw8GigZSEnM2u",
|
||||
"model": "gpt-4o-2024-08-06",
|
||||
"object": "chat.completion",
|
||||
"system_fingerprint": "fp_67802d9a6d",
|
||||
"usage": {
|
||||
"completion_tokens": 7,
|
||||
"prompt_tokens": 24,
|
||||
"total_tokens": 31
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**文本向量请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"input": ["first phrase", "second phrase", "third phrase"],
|
||||
"model": "text-embedding-3-large"
|
||||
}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"index": 0,
|
||||
"embedding": [
|
||||
-0.0012583479,
|
||||
0.0020349282,
|
||||
...
|
||||
0.012051377,
|
||||
-0.0053306012,
|
||||
0.0060688322
|
||||
]
|
||||
}
|
||||
],
|
||||
"model": "text-embedding-3-large",
|
||||
"usage": {
|
||||
"prompt_tokens": 6,
|
||||
"total_tokens": 6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理360智脑服务
|
||||
|
||||
**配置信息**
|
||||
@@ -961,7 +1143,7 @@ provider:
|
||||
provider:
|
||||
type: ai360
|
||||
apiTokens:
|
||||
- "YOUR_MINIMAX_API_TOKEN"
|
||||
- "YOUR_360_API_TOKEN"
|
||||
modelMapping:
|
||||
"gpt-4o": "360gpt-turbo-responsibility-8k"
|
||||
"gpt-4": "360gpt2-pro"
|
||||
|
||||
@@ -139,9 +139,9 @@ For 360 Brain, the corresponding `type` is `ai360`. It has no unique configurati
|
||||
|
||||
For Mistral, the corresponding `type` is `mistral`. It has no unique configuration fields.
|
||||
|
||||
#### Minimax
|
||||
#### MiniMax
|
||||
|
||||
For Minimax, the corresponding `type` is `minimax`. Its unique configuration field is:
|
||||
For MiniMax, the corresponding `type` is `minimax`. Its unique configuration field is:
|
||||
|
||||
| Name | Data Type | Filling Requirements | Default Value | Description |
|
||||
| ---------------- | -------- | --------------------- |---------------|------------------------------------------------------------------------------------------------------------|
|
||||
@@ -593,6 +593,79 @@ provider:
|
||||
"request_id": "187e99ba-5b64-9ffe-8f69-01dafbaf6ed7"
|
||||
}
|
||||
```
|
||||
|
||||
### Forwards requests to AliCloud Bailian with the "original" protocol
|
||||
|
||||
**Configuration Information**
|
||||
|
||||
```yaml
|
||||
activeProviderId: my-qwen
|
||||
providers:
|
||||
- id: my-qwen
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "YOUR_DASHSCOPE_API_TOKEN"
|
||||
protocol: original
|
||||
```
|
||||
|
||||
**Example Request**
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"prompt": "What is Dubbo?"
|
||||
},
|
||||
"parameters": {},
|
||||
"debug": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"finish_reason": "stop",
|
||||
"session_id": "677e7e8fbb874e1b84792b65042e1599",
|
||||
"text": "Apache Dubbo is a..."
|
||||
},
|
||||
"usage": {
|
||||
"models": [
|
||||
{
|
||||
"output_tokens": 449,
|
||||
"model_id": "qwen-max",
|
||||
"input_tokens": 282
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "b59e45e3-5af4-91df-b7c6-9d746fd3297c"
|
||||
}
|
||||
```
|
||||
|
||||
### Using OpenAI Protocol Proxy for Doubao Service
|
||||
|
||||
```yaml
|
||||
activeProviderId: my-doubao
|
||||
providers:
|
||||
- id: my-doubao
|
||||
type: doubao
|
||||
apiTokens:
|
||||
- YOUR_DOUBAO_API_KEY
|
||||
modelMapping:
|
||||
'*': YOUR_DOUBAO_ENDPOINT
|
||||
timeout: 1200000
|
||||
```
|
||||
|
||||
### Using original Protocol Proxy for Coze applications
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: coze
|
||||
apiTokens:
|
||||
- YOUR_COZE_API_KEY
|
||||
protocol: original
|
||||
```
|
||||
|
||||
### Utilizing Moonshot with its Native File Context
|
||||
|
||||
Upload files to Moonshot in advance and use its AI services based on file content.
|
||||
@@ -782,8 +855,7 @@ provider:
|
||||
|
||||
Request script:
|
||||
|
||||
```sh
|
||||
|
||||
```shell
|
||||
curl --location 'http://<your higress domain>/v1/chat/completions' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
@@ -955,7 +1027,7 @@ provider:
|
||||
provider:
|
||||
type: ai360
|
||||
apiTokens:
|
||||
- "YOUR_MINIMAX_API_TOKEN"
|
||||
- "YOUR_AI360_API_TOKEN"
|
||||
modelMapping:
|
||||
"gpt-4o": "360gpt-turbo-responsibility-8k"
|
||||
"gpt-4": "360gpt2-pro"
|
||||
@@ -1264,6 +1336,7 @@ Here, `model` denotes the service tier of DeepL and can only be either `Free` or
|
||||
```
|
||||
|
||||
**Response Example**
|
||||
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
|
||||
@@ -25,32 +25,70 @@ import (
|
||||
type PluginConfig struct {
|
||||
// @Title zh-CN AI服务提供商配置
|
||||
// @Description zh-CN AI服务提供商配置,包含API接口、模型和知识库文件等信息
|
||||
providerConfig provider.ProviderConfig `required:"true" yaml:"provider"`
|
||||
providerConfigs []provider.ProviderConfig `required:"true" yaml:"providers"`
|
||||
|
||||
provider provider.Provider `yaml:"-"`
|
||||
activeProviderConfig *provider.ProviderConfig `yaml:"-"`
|
||||
activeProvider provider.Provider `yaml:"-"`
|
||||
}
|
||||
|
||||
func (c *PluginConfig) FromJson(json gjson.Result) {
|
||||
c.providerConfig.FromJson(json.Get("provider"))
|
||||
if providersJson := json.Get("providers"); providersJson.Exists() && providersJson.IsArray() {
|
||||
c.providerConfigs = make([]provider.ProviderConfig, 0)
|
||||
for _, providerJson := range providersJson.Array() {
|
||||
providerConfig := provider.ProviderConfig{}
|
||||
providerConfig.FromJson(providerJson)
|
||||
c.providerConfigs = append(c.providerConfigs, providerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
if providerJson := json.Get("provider"); providerJson.Exists() && providerJson.IsObject() {
|
||||
// TODO: For legacy config support. To be removed later.
|
||||
providerConfig := provider.ProviderConfig{}
|
||||
providerConfig.FromJson(providerJson)
|
||||
c.providerConfigs = []provider.ProviderConfig{providerConfig}
|
||||
c.activeProviderConfig = &providerConfig
|
||||
// Legacy configuration is used and the active provider is determined.
|
||||
// We don't need to continue with the new configuration style.
|
||||
return
|
||||
}
|
||||
|
||||
c.activeProviderConfig = nil
|
||||
|
||||
activeProviderId := json.Get("activeProviderId").String()
|
||||
if activeProviderId != "" {
|
||||
for _, providerConfig := range c.providerConfigs {
|
||||
if providerConfig.GetId() == activeProviderId {
|
||||
c.activeProviderConfig = &providerConfig
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PluginConfig) Validate() error {
|
||||
if err := c.providerConfig.Validate(); err != nil {
|
||||
if c.activeProviderConfig == nil {
|
||||
return nil
|
||||
}
|
||||
if err := c.activeProviderConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PluginConfig) Complete() error {
|
||||
if c.activeProviderConfig == nil {
|
||||
c.activeProvider = nil
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
c.provider, err = provider.CreateProvider(c.providerConfig)
|
||||
c.activeProvider, err = provider.CreateProvider(*c.activeProviderConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetProvider() provider.Provider {
|
||||
return c.provider
|
||||
return c.activeProvider
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetProviderConfig() provider.ProviderConfig {
|
||||
return c.providerConfig
|
||||
func (c *PluginConfig) GetProviderConfig() *provider.ProviderConfig {
|
||||
return c.activeProviderConfig
|
||||
}
|
||||
|
||||
@@ -56,21 +56,24 @@ static_resources:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
{
|
||||
"provider": {
|
||||
"type": "moonshot",
|
||||
"domain": "api.moonshot.cn",
|
||||
"apiTokens": [
|
||||
"****",
|
||||
"****"
|
||||
],
|
||||
"timeout": 1200000,
|
||||
"modelMapping": {
|
||||
"gpt-3": "moonshot-v1-8k",
|
||||
"gpt-35-turbo": "moonshot-v1-32k",
|
||||
"gpt-4-turbo": "moonshot-v1-128k",
|
||||
"*": "moonshot-v1-8k"
|
||||
},
|
||||
}
|
||||
"activeProviderId": "moonshot",
|
||||
"providers": [
|
||||
{
|
||||
"type": "moonshot",
|
||||
"domain": "api.moonshot.cn",
|
||||
"apiTokens": [
|
||||
"****",
|
||||
"****"
|
||||
],
|
||||
"timeout": 1200000,
|
||||
"modelMapping": {
|
||||
"gpt-3": "moonshot-v1-8k",
|
||||
"gpt-35-turbo": "moonshot-v1-32k",
|
||||
"gpt-4-turbo": "moonshot-v1-128k",
|
||||
"*": "moonshot-v1-8k"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
- name: envoy.filters.http.router
|
||||
clusters:
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -13,8 +13,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
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=
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
const (
|
||||
pluginName = "ai-proxy"
|
||||
|
||||
ctxKeyApiName = "apiKey"
|
||||
ctxKeyApiName = "apiName"
|
||||
|
||||
defaultMaxBodyBytes uint32 = 10 * 1024 * 1024
|
||||
)
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
pluginName,
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeader),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
@@ -37,8 +37,23 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, pluginConfig *config.PluginConfig, log wrapper.Log) error {
|
||||
// log.Debugf("loading config: %s", json.String())
|
||||
func parseGlobalConfig(json gjson.Result, pluginConfig *config.PluginConfig, log wrapper.Log) error {
|
||||
//log.Debugf("loading global config: %s", json.String())
|
||||
|
||||
pluginConfig.FromJson(json)
|
||||
if err := pluginConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pluginConfig.Complete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOverrideRuleConfig(json gjson.Result, global config.PluginConfig, pluginConfig *config.PluginConfig, log wrapper.Log) error {
|
||||
//log.Debugf("loading override rule config: %s", json.String())
|
||||
|
||||
*pluginConfig = global
|
||||
|
||||
pluginConfig.FromJson(json)
|
||||
if err := pluginConfig.Validate(); err != nil {
|
||||
@@ -64,9 +79,11 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
|
||||
rawPath := ctx.Path()
|
||||
path, _ := url.Parse(rawPath)
|
||||
apiName := getOpenAiApiName(path.Path)
|
||||
if apiName == "" {
|
||||
providerConfig := pluginConfig.GetProviderConfig()
|
||||
if apiName == "" && !providerConfig.IsOriginal() {
|
||||
log.Debugf("[onHttpRequestHeader] unsupported path: %s", path.Path)
|
||||
_ = util.SendResponse(404, "ai-proxy.unknown_api", util.MimeTypeTextPlain, "API not found: "+path.Path)
|
||||
// _ = util.SendResponse(404, "ai-proxy.unknown_api", util.MimeTypeTextPlain, "API not found: "+path.Path)
|
||||
log.Debugf("[onHttpRequestHeader] no send response")
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.SetContext(ctxKeyApiName, apiName)
|
||||
|
||||
114
plugins/wasm-go/extensions/ai-proxy/provider/cohere.go
Normal file
114
plugins/wasm-go/extensions/ai-proxy/provider/cohere.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
cohereDomain = "api.cohere.com"
|
||||
chatCompletionPath = "/v1/chat"
|
||||
)
|
||||
|
||||
type cohereProviderInitializer struct{}
|
||||
|
||||
func (m *cohereProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *cohereProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &cohereProvider{
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type cohereProvider struct {
|
||||
config ProviderConfig
|
||||
}
|
||||
|
||||
type cohereTextGenRequest struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
K int `json:"k,omitempty"`
|
||||
P float64 `json:"p,omitempty"`
|
||||
Seed int `json:"seed,omitempty"`
|
||||
StopSequences []string `json:"stop_sequences,omitempty"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||
}
|
||||
|
||||
func (m *cohereProvider) GetProviderType() string {
|
||||
return providerTypeCohere
|
||||
}
|
||||
|
||||
func (m *cohereProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
_ = util.OverwriteRequestHost(cohereDomain)
|
||||
_ = util.OverwriteRequestPath(chatCompletionPath)
|
||||
_ = util.OverwriteRequestAuthorization("Bearer " + m.config.GetRandomToken())
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (m *cohereProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
if m.config.protocol == protocolOriginal {
|
||||
request := &cohereTextGenRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
return m.handleRequestBody(log, request)
|
||||
}
|
||||
origin := &chatCompletionRequest{}
|
||||
if err := decodeChatCompletionRequest(body, origin); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
request := m.buildCohereRequest(origin)
|
||||
return m.handleRequestBody(log, request)
|
||||
}
|
||||
|
||||
func (m *cohereProvider) handleRequestBody(log wrapper.Log, request interface{}) (types.Action, error) {
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
}()
|
||||
err := replaceJsonRequestBody(request, log)
|
||||
if err != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.cohere.proxy_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
}
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
|
||||
func (m *cohereProvider) buildCohereRequest(origin *chatCompletionRequest) *cohereTextGenRequest {
|
||||
if len(origin.Messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &cohereTextGenRequest{
|
||||
Message: origin.Messages[0].StringContent(),
|
||||
Model: origin.Model,
|
||||
MaxTokens: origin.MaxTokens,
|
||||
Stream: origin.Stream,
|
||||
Temperature: origin.Temperature,
|
||||
K: origin.N,
|
||||
P: origin.TopP,
|
||||
Seed: origin.Seed,
|
||||
StopSequences: origin.Stop,
|
||||
FrequencyPenalty: origin.FrequencyPenalty,
|
||||
PresencePenalty: origin.PresencePenalty,
|
||||
}
|
||||
}
|
||||
44
plugins/wasm-go/extensions/ai-proxy/provider/coze.go
Normal file
44
plugins/wasm-go/extensions/ai-proxy/provider/coze.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
cozeDomain = "api.coze.cn"
|
||||
)
|
||||
|
||||
type cozeProviderInitializer struct{}
|
||||
|
||||
func (m *cozeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *cozeProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &cozeProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type cozeProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (m *cozeProvider) GetProviderType() string {
|
||||
return providerTypeCoze
|
||||
}
|
||||
|
||||
func (m *cozeProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
_ = util.OverwriteRequestHost(cozeDomain)
|
||||
_ = util.OverwriteRequestAuthorization("Bearer " + m.config.GetRandomToken())
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user