mirror of
https://github.com/alibaba/higress.git
synced 2026-03-20 11:08:03 +08:00
Compare commits
108 Commits
v2.0.0
...
wasm-go-ai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e68a8ac25f | ||
|
|
96575b982e | ||
|
|
c2d405b2a7 | ||
|
|
6efb3109f2 | ||
|
|
1b1c08afb7 | ||
|
|
d24123a55f | ||
|
|
f2a5df3949 | ||
|
|
ebc5b2987e | ||
|
|
ca97cbd75a | ||
|
|
a787e237ce | ||
|
|
6a1bf90d42 | ||
|
|
60e476da87 | ||
|
|
2cb8558cda | ||
|
|
4d1a037942 | ||
|
|
39b6eac9d0 | ||
|
|
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,42 @@ 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 }}"
|
||||
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}')
|
||||
fi
|
||||
|
||||
if [[ "$plugin_type" == "rust" ]]; then
|
||||
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-rust-builder:rust${{ env.RUST_VERSION }}-oras${{ env.ORAS_VERSION }}"
|
||||
else
|
||||
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 +92,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 +106,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 +118,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 +127,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"
|
||||
|
||||
35
.github/workflows/helm-docs.yaml
vendored
Normal file
35
.github/workflows/helm-docs.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: "Helm Docs"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
push:
|
||||
|
||||
jobs:
|
||||
|
||||
helm:
|
||||
name: Helm Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22.9'
|
||||
|
||||
- name: Run helm-docs
|
||||
run: |
|
||||
GOBIN=$PWD GO111MODULE=on go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
|
||||
./helm-docs -c ${GITHUB_WORKSPACE}/helm/higress -f ../core/values.yaml
|
||||
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/*md)
|
||||
if [ ! -z "$DIFF" ]; then
|
||||
echo "Please use helm-docs in your clone, of your fork, of the project, and commit a updated README.md for the chart."
|
||||
fi
|
||||
git diff --exit-code
|
||||
rm -f ./helm-docs
|
||||
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
.gitignore
vendored
3
.gitignore
vendored
@@ -16,4 +16,5 @@ helm/**/charts/**.tgz
|
||||
target/
|
||||
tools/hack/cluster.conf
|
||||
envoy/1.20
|
||||
istio/1.12
|
||||
istio/1.12
|
||||
Cargo.lock
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
55
README.md
55
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,32 +60,45 @@ 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/)。
|
||||
|
||||
如果您是在云上部署,生产环境推荐使用[企业版](https://higress.io/cloud/),开发测试可以使用下面一键部署社区版:
|
||||
|
||||
[](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Higress社区版)
|
||||
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **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 +180,7 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||
### 交流群
|
||||
|
||||

|
||||

|
||||
|
||||
### 技术分享
|
||||
|
||||
@@ -173,3 +188,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 }}
|
||||
@@ -10,7 +10,7 @@ global:
|
||||
onDemandRDS: false
|
||||
hostRDSMergeSubset: false
|
||||
onlyPushRouteCluster: true
|
||||
# IngressClass filters which ingress resources the higress controller watches.
|
||||
# -- IngressClass filters which ingress resources the higress controller watches.
|
||||
# The default ingress class is higress.
|
||||
# There are some special cases for special ingress class.
|
||||
# 1. When the ingress class is set as nginx, the higress controller will watch ingress
|
||||
@@ -18,28 +18,40 @@ global:
|
||||
# 2. When the ingress class is set empty, the higress controller will watch all ingress
|
||||
# resources in the k8s cluster.
|
||||
ingressClass: "higress"
|
||||
# -- If not empty, Higress Controller will only watch resources in the specified namespace.
|
||||
# When isolating different business systems using K8s namespace,
|
||||
# if each namespace requires a standalone gateway instance,
|
||||
# this parameter can be used to confine the Ingress watching of Higress within the given namespace.
|
||||
watchNamespace: ""
|
||||
# -- Whether to disable HTTP/2 in ALPN
|
||||
disableAlpnH2: false
|
||||
# -- If true, Higress Controller will update the status field of Ingress resources.
|
||||
# When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten,
|
||||
# this parameter needs to be set to false,
|
||||
# so Higress won't write the entry IP to the status field of the corresponding Ingress object.
|
||||
enableStatus: true
|
||||
# whether to use autoscaling/v2 template for HPA settings
|
||||
# -- whether to use autoscaling/v2 template for HPA settings
|
||||
# for internal usage only, not to be configured by users.
|
||||
autoscalingv2API: true
|
||||
local: false # When deploying to a local cluster (e.g.: kind cluster), set this to true.
|
||||
# -- When deploying to a local cluster (e.g.: kind cluster), set this to true.
|
||||
local: false
|
||||
kind: false # Deprecated. Please use "global.local" instead. Will be removed later.
|
||||
enableIstioAPI: false
|
||||
# -- If true, Higress Controller will monitor istio resources as well
|
||||
enableIstioAPI: true
|
||||
# -- If true, Higress Controller will monitor Gateway API resources as well
|
||||
enableGatewayAPI: false
|
||||
# Deprecated
|
||||
enableHigressIstio: false
|
||||
# Used to locate istiod.
|
||||
# -- Used to locate istiod.
|
||||
istioNamespace: istio-system
|
||||
# enable pod disruption budget for the control plane, which is used to
|
||||
# -- enable pod disruption budget for the control plane, which is used to
|
||||
# ensure Istio control plane components are gradually upgraded or recovered.
|
||||
defaultPodDisruptionBudget:
|
||||
enabled: false
|
||||
# The values aren't mutable due to a current PodDisruptionBudget limitation
|
||||
# minAvailable: 1
|
||||
|
||||
# A minimal set of requested resources to applied to all deployments so that
|
||||
# -- A minimal set of requested resources to applied to all deployments so that
|
||||
# Horizontal Pod Autoscaler will be able to function (if set).
|
||||
# Each component can overwrite these default values by adding its own resources
|
||||
# block in the relevant section below and setting the desired resources values.
|
||||
@@ -51,16 +63,16 @@ global:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# Default hub for Istio images.
|
||||
# -- Default hub for Istio images.
|
||||
# Releases are published to docker hub under 'istio' project.
|
||||
# Dev builds from prow are on gcr.io
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
|
||||
# Specify image pull policy if default behavior isn't desired.
|
||||
# -- Specify image pull policy if default behavior isn't desired.
|
||||
# Default behavior: latest images will be Always else IfNotPresent.
|
||||
imagePullPolicy: ""
|
||||
|
||||
# ImagePullSecrets for all ServiceAccount, list of secrets in the same namespace
|
||||
# -- ImagePullSecrets for all ServiceAccount, list of secrets in the same namespace
|
||||
# to use for pulling any images in pods that reference this ServiceAccount.
|
||||
# For components that don't use ServiceAccounts (i.e. grafana, servicegraph, tracing)
|
||||
# ImagePullSecrets will be added to the corresponding Deployment(StatefulSet) objects.
|
||||
@@ -68,14 +80,14 @@ global:
|
||||
imagePullSecrets: []
|
||||
# - private-registry-key
|
||||
|
||||
# Enabled by default in master for maximising testing.
|
||||
# -- Enabled by default in master for maximising testing.
|
||||
istiod:
|
||||
enableAnalysis: false
|
||||
|
||||
# To output all istio components logs in json format by adding --log_as_json argument to each container argument
|
||||
# -- To output all istio components logs in json format by adding --log_as_json argument to each container argument
|
||||
logAsJson: false
|
||||
|
||||
# Comma-separated minimum per-scope logging level of messages to output, in the form of <scope>:<level>,<scope>:<level>
|
||||
# -- Comma-separated minimum per-scope logging level of messages to output, in the form of <scope>:<level>,<scope>:<level>
|
||||
# The control plane has different scopes depending on component, but can configure default log level across all components
|
||||
# If empty, default scope and level will be used as configured in code
|
||||
logging:
|
||||
@@ -83,11 +95,11 @@ global:
|
||||
|
||||
omitSidecarInjectorConfigMap: false
|
||||
|
||||
# Whether to restrict the applications namespace the controller manages;
|
||||
# -- Whether to restrict the applications namespace the controller manages;
|
||||
# If not set, controller watches all namespaces
|
||||
oneNamespace: false
|
||||
|
||||
# Configure whether Operator manages webhook configurations. The current behavior
|
||||
# -- Configure whether Operator manages webhook configurations. The current behavior
|
||||
# of Istiod is to manage its own webhook configurations.
|
||||
# When this option is set as true, Istio Operator, instead of webhooks, manages the
|
||||
# webhook configurations. When this option is set as false, webhooks manage their
|
||||
@@ -106,7 +118,7 @@ global:
|
||||
#- global
|
||||
#- "{{ valueOrDefault .DeploymentMeta.Namespace \"default\" }}.global"
|
||||
|
||||
# Kubernetes >=v1.11.0 will create two PriorityClass, including system-cluster-critical and
|
||||
# -- Kubernetes >=v1.11.0 will create two PriorityClass, including system-cluster-critical and
|
||||
# system-node-critical, it is better to configure this in order to make sure your Istio pods
|
||||
# will not be killed because of low priority class.
|
||||
# Refer to https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass
|
||||
@@ -116,18 +128,18 @@ global:
|
||||
proxy:
|
||||
image: proxyv2
|
||||
|
||||
# This controls the 'policy' in the sidecar injector.
|
||||
# -- This controls the 'policy' in the sidecar injector.
|
||||
autoInject: enabled
|
||||
|
||||
# CAUTION: It is important to ensure that all Istio helm charts specify the same clusterDomain value
|
||||
# -- CAUTION: It is important to ensure that all Istio helm charts specify the same clusterDomain value
|
||||
# cluster domain. Default value is "cluster.local".
|
||||
clusterDomain: "cluster.local"
|
||||
|
||||
# Per Component log level for proxy, applies to gateways and sidecars. If a component level is
|
||||
# -- Per Component log level for proxy, applies to gateways and sidecars. If a component level is
|
||||
# not set, then the global "logLevel" will be used.
|
||||
componentLogLevel: "misc:error"
|
||||
|
||||
# If set, newly injected sidecars will have core dumps enabled.
|
||||
# -- If set, newly injected sidecars will have core dumps enabled.
|
||||
enableCoreDump: false
|
||||
|
||||
# istio ingress capture allowlist
|
||||
@@ -136,8 +148,7 @@ global:
|
||||
excludeInboundPorts: ""
|
||||
includeInboundPorts: "*"
|
||||
|
||||
|
||||
# istio egress capture allowlist
|
||||
# -- 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"
|
||||
# would only capture egress traffic on those two IP Ranges, all other outbound traffic would
|
||||
@@ -147,29 +158,29 @@ global:
|
||||
includeOutboundPorts: ""
|
||||
excludeOutboundPorts: ""
|
||||
|
||||
# Log level for proxy, applies to gateways and sidecars.
|
||||
# -- Log level for proxy, applies to gateways and sidecars.
|
||||
# Expected values are: trace|debug|info|warning|error|critical|off
|
||||
logLevel: warning
|
||||
|
||||
#If set to true, istio-proxy container will have privileged securityContext
|
||||
# -- If set to true, istio-proxy container will have privileged securityContext
|
||||
privileged: false
|
||||
|
||||
# The number of successive failed probes before indicating readiness failure.
|
||||
# -- The number of successive failed probes before indicating readiness failure.
|
||||
readinessFailureThreshold: 30
|
||||
|
||||
# The number of successive successed probes before indicating readiness success.
|
||||
# -- The number of successive successed probes before indicating readiness success.
|
||||
readinessSuccessThreshold: 30
|
||||
|
||||
# The initial delay for readiness probes in seconds.
|
||||
# -- The initial delay for readiness probes in seconds.
|
||||
readinessInitialDelaySeconds: 1
|
||||
|
||||
# The period between readiness probes.
|
||||
# -- The period between readiness probes.
|
||||
readinessPeriodSeconds: 2
|
||||
|
||||
# The readiness timeout seconds
|
||||
# -- The readiness timeout seconds
|
||||
readinessTimeoutSeconds: 3
|
||||
|
||||
# Resources for the sidecar.
|
||||
# -- Resources for the sidecar.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
@@ -178,18 +189,18 @@ global:
|
||||
cpu: 2000m
|
||||
memory: 1024Mi
|
||||
|
||||
# Default port for Pilot agent health checks. A value of 0 will disable health checking.
|
||||
# -- Default port for Pilot agent health checks. A value of 0 will disable health checking.
|
||||
statusPort: 15020
|
||||
|
||||
# Specify which tracer to use. One of: lightstep, datadog, stackdriver.
|
||||
# -- Specify which tracer to use. One of: lightstep, datadog, stackdriver.
|
||||
# If using stackdriver tracer outside GCP, set env GOOGLE_APPLICATION_CREDENTIALS to the GCP credential file.
|
||||
tracer: ""
|
||||
|
||||
# Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
|
||||
# -- Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
|
||||
holdApplicationUntilProxyStarts: false
|
||||
|
||||
proxy_init:
|
||||
# Base name for the proxy_init container, used to configure iptables.
|
||||
# -- Base name for the proxy_init container, used to configure iptables.
|
||||
image: proxyv2
|
||||
resources:
|
||||
limits:
|
||||
@@ -199,7 +210,7 @@ global:
|
||||
cpu: 10m
|
||||
memory: 10Mi
|
||||
|
||||
# configure remote pilot and istiod service and endpoint
|
||||
# -- configure remote pilot and istiod service and endpoint
|
||||
remotePilotAddress: ""
|
||||
|
||||
##############################################################################################
|
||||
@@ -207,20 +218,20 @@ global:
|
||||
# make sure they are consistent across your Istio helm charts #
|
||||
##############################################################################################
|
||||
|
||||
# The customized CA address to retrieve certificates for the pods in the cluster.
|
||||
# -- The customized CA address to retrieve certificates for the pods in the cluster.
|
||||
# CSR clients such as the Istio Agent and ingress gateways can use this to specify the CA endpoint.
|
||||
# If not set explicitly, default to the Istio discovery address.
|
||||
caAddress: ""
|
||||
|
||||
# Configure a remote cluster data plane controlled by an external istiod.
|
||||
# -- Configure a remote cluster data plane controlled by an external istiod.
|
||||
# When set to true, istiod is not deployed locally and only a subset of the other
|
||||
# discovery charts are enabled.
|
||||
externalIstiod: false
|
||||
|
||||
# Configure a remote cluster as the config cluster for an external istiod.
|
||||
# -- Configure a remote cluster as the config cluster for an external istiod.
|
||||
configCluster: false
|
||||
|
||||
# Configure the policy for validating JWT.
|
||||
# -- Configure the policy for validating JWT.
|
||||
# Currently, two options are supported: "third-party-jwt" and "first-party-jwt".
|
||||
jwtPolicy: "third-party-jwt"
|
||||
|
||||
@@ -242,7 +253,7 @@ global:
|
||||
# of migration TBD, and it may be a disruptive operation to change the Mesh
|
||||
# ID post-install.
|
||||
#
|
||||
# If the mesh admin does not specify a value, Istio will use the value of the
|
||||
# -- If the mesh admin does not specify a value, Istio will use the value of the
|
||||
# mesh's Trust Domain. The best practice is to select a proper Trust Domain
|
||||
# value.
|
||||
meshID: ""
|
||||
@@ -276,68 +287,69 @@ global:
|
||||
#
|
||||
meshNetworks: {}
|
||||
|
||||
# Use the user-specified, secret volume mounted key and certs for Pilot and workloads.
|
||||
# -- Use the user-specified, secret volume mounted key and certs for Pilot and workloads.
|
||||
mountMtlsCerts: false
|
||||
|
||||
multiCluster:
|
||||
# Set to true to connect two kubernetes clusters via their respective
|
||||
# -- Set to true to connect two kubernetes clusters via their respective
|
||||
# ingressgateway services when pods in each cluster cannot directly
|
||||
# talk to one another. All clusters should be using Istio mTLS and must
|
||||
# have a shared root CA for this model to work.
|
||||
enabled: true
|
||||
# Should be set to the name of the cluster this installation will run in. This is required for sidecar injection
|
||||
# -- Should be set to the name of the cluster this installation will run in. This is required for sidecar injection
|
||||
# to properly label proxies
|
||||
clusterName: ""
|
||||
|
||||
# Network defines the network this cluster belong to. This name
|
||||
# -- Network defines the network this cluster belong to. This name
|
||||
# corresponds to the networks in the map of mesh networks.
|
||||
network: ""
|
||||
|
||||
# Configure the certificate provider for control plane communication.
|
||||
# -- Configure the certificate provider for control plane communication.
|
||||
# Currently, two providers are supported: "kubernetes" and "istiod".
|
||||
# As some platforms may not have kubernetes signing APIs,
|
||||
# Istiod is the default
|
||||
pilotCertProvider: istiod
|
||||
|
||||
sds:
|
||||
# The JWT token for SDS and the aud field of such JWT. See RFC 7519, section 4.1.3.
|
||||
# -- The JWT token for SDS and the aud field of such JWT. See RFC 7519, section 4.1.3.
|
||||
# When a CSR is sent from Istio Agent to the CA (e.g. Istiod), this aud is to make sure the
|
||||
# JWT is intended for the CA.
|
||||
token:
|
||||
aud: istio-ca
|
||||
|
||||
sts:
|
||||
# The service port used by Security Token Service (STS) server to handle token exchange requests.
|
||||
# -- The service port used by Security Token Service (STS) server to handle token exchange requests.
|
||||
# Setting this port to a non-zero value enables STS server.
|
||||
servicePort: 0
|
||||
|
||||
# Configuration for each of the supported tracers
|
||||
# -- Configuration for each of the supported tracers
|
||||
tracer:
|
||||
# Configuration for envoy to send trace data to LightStep.
|
||||
# -- Configuration for envoy to send trace data to LightStep.
|
||||
# Disabled by default.
|
||||
# address: the <host>:<port> of the satellite pool
|
||||
# accessToken: required for sending data to the pool
|
||||
#
|
||||
datadog:
|
||||
# Host:Port for submitting traces to the Datadog agent.
|
||||
# -- Host:Port for submitting traces to the Datadog agent.
|
||||
address: "$(HOST_IP):8126"
|
||||
lightstep:
|
||||
address: "" # example: lightstep-satellite:443
|
||||
accessToken: "" # example: abcdefg1234567
|
||||
# -- example: lightstep-satellite:443
|
||||
address: ""
|
||||
# -- example: abcdefg1234567
|
||||
accessToken: ""
|
||||
stackdriver:
|
||||
# enables trace output to stdout.
|
||||
# -- enables trace output to stdout.
|
||||
debug: false
|
||||
# The global default max number of message events per span.
|
||||
# -- The global default max number of message events per span.
|
||||
maxNumberOfMessageEvents: 200
|
||||
# The global default max number of annotation events per span.
|
||||
# -- The global default max number of annotation events per span.
|
||||
maxNumberOfAnnotations: 200
|
||||
# The global default max number of attributes per span.
|
||||
# -- The global default max number of attributes per span.
|
||||
maxNumberOfAttributes: 200
|
||||
# Use the Mesh Control Protocol (MCP) for configuring Istiod. Requires an MCP source.
|
||||
|
||||
# -- Use the Mesh Control Protocol (MCP) for configuring Istiod. Requires an MCP source.
|
||||
useMCP: false
|
||||
|
||||
# Observability (o11y) configurations
|
||||
# -- Observability (o11y) configurations
|
||||
o11y:
|
||||
enabled: false
|
||||
promtail:
|
||||
@@ -351,7 +363,7 @@ global:
|
||||
memory: 2Gi
|
||||
securityContext: {}
|
||||
|
||||
# The name of the CA for workload certificates.
|
||||
# -- The name of the CA for workload certificates.
|
||||
# For example, when caName=GkeWorkloadCertificate, GKE workload certificates
|
||||
# will be used as the certificates for workloads.
|
||||
# The default value is "" and when caName="", the CA will be configured by other
|
||||
@@ -360,7 +372,7 @@ global:
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
|
||||
clusterName: ""
|
||||
# meshConfig defines runtime configuration of components, including Istiod and istio-agent behavior
|
||||
# -- meshConfig defines runtime configuration of components, including Istiod and istio-agent behavior
|
||||
# See https://istio.io/docs/reference/config/istio.mesh.v1alpha1/ for all available options
|
||||
meshConfig:
|
||||
enablePrometheusMerge: true
|
||||
@@ -371,14 +383,13 @@ meshConfig:
|
||||
# and gradual adoption by setting capture only on specific workloads. It also allows
|
||||
# VMs to use other DNS options, like dnsmasq or unbound.
|
||||
|
||||
# The namespace to treat as the administrative root namespace for Istio configuration.
|
||||
# -- The namespace to treat as the administrative root namespace for Istio configuration.
|
||||
# When processing a leaf namespace Istio will search for declarations in that namespace first
|
||||
# and if none are found it will search in the root namespace. Any matching declaration found in the root namespace
|
||||
# is processed as if it were declared in the leaf namespace.
|
||||
|
||||
rootNamespace:
|
||||
|
||||
# The trust domain corresponds to the trust root of a system
|
||||
# -- The trust domain corresponds to the trust root of a system
|
||||
# Refer to https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md#21-trust-domain
|
||||
trustDomain: "cluster.local"
|
||||
|
||||
@@ -392,56 +403,57 @@ meshConfig:
|
||||
|
||||
gateway:
|
||||
name: "higress-gateway"
|
||||
# -- Number of Higress Gateway pods
|
||||
replicas: 2
|
||||
image: gateway
|
||||
|
||||
# -- Use a `DaemonSet` or `Deployment`
|
||||
kind: Deployment
|
||||
|
||||
# The number of successive failed probes before indicating readiness failure.
|
||||
# -- The number of successive failed probes before indicating readiness failure.
|
||||
readinessFailureThreshold: 30
|
||||
|
||||
# The number of successive successed probes before indicating readiness success.
|
||||
# -- The number of successive successed probes before indicating readiness success.
|
||||
readinessSuccessThreshold: 1
|
||||
|
||||
# The initial delay for readiness probes in seconds.
|
||||
# -- The initial delay for readiness probes in seconds.
|
||||
readinessInitialDelaySeconds: 1
|
||||
|
||||
# The period between readiness probes.
|
||||
# -- The period between readiness probes.
|
||||
readinessPeriodSeconds: 2
|
||||
|
||||
# The readiness timeout seconds
|
||||
# -- The readiness timeout seconds
|
||||
readinessTimeoutSeconds: 3
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
tag: ""
|
||||
# revision declares which revision this gateway is a part of
|
||||
# -- revision declares which revision this gateway is a part of
|
||||
revision: ""
|
||||
|
||||
rbac:
|
||||
# If enabled, roles will be created to enable accessing certificates from Gateways. This is not needed
|
||||
# -- If enabled, roles will be created to enable accessing certificates from Gateways. This is not needed
|
||||
# when using http://gateway-api.org/.
|
||||
enabled: true
|
||||
|
||||
serviceAccount:
|
||||
# If set, a service account will be created. Otherwise, the default is used
|
||||
# -- If set, a service account will be created. Otherwise, the default is used
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# -- The name of the service account to use.
|
||||
# If not set, the release name is used
|
||||
name: ""
|
||||
|
||||
# Pod environment variables
|
||||
# -- Pod environment variables
|
||||
env: {}
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
hostNetwork: false
|
||||
|
||||
# Labels to apply to all resources
|
||||
# -- Labels to apply to all resources
|
||||
labels: {}
|
||||
|
||||
# Annotations to apply to all resources
|
||||
# -- Annotations to apply to all resources
|
||||
annotations: {}
|
||||
|
||||
podAnnotations:
|
||||
@@ -449,25 +461,25 @@ gateway:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/path: "/stats/prometheus"
|
||||
sidecar.istio.io/inject: "false"
|
||||
|
||||
# Define the security context for the pod.
|
||||
|
||||
# -- 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 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 +488,7 @@ gateway:
|
||||
|
||||
rollingMaxSurge: 100%
|
||||
rollingMaxUnavailable: 25%
|
||||
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 2000m
|
||||
@@ -484,24 +496,42 @@ 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.
|
||||
|
||||
# -- 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"
|
||||
# -- Number of Higress Controller pods
|
||||
replicas: 1
|
||||
image: higress
|
||||
|
||||
@@ -510,61 +540,52 @@ 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
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
# -- 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 +600,11 @@ controller:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2048Mi
|
||||
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
|
||||
tolerations: []
|
||||
|
||||
|
||||
affinity: {}
|
||||
|
||||
autoscaling:
|
||||
@@ -594,8 +615,8 @@ controller:
|
||||
automaticHttps:
|
||||
enabled: true
|
||||
email: ""
|
||||
|
||||
## Discovery Settings
|
||||
|
||||
## -- Discovery Settings
|
||||
pilot:
|
||||
autoscaleEnabled: false
|
||||
autoscaleMin: 1
|
||||
@@ -607,11 +628,11 @@ pilot:
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
tag: ""
|
||||
|
||||
# Can be a full hub/image:tag
|
||||
# -- Can be a full hub/image:tag
|
||||
image: pilot
|
||||
traceSampling: 1.0
|
||||
|
||||
# Resources for a small pilot install
|
||||
# -- Resources for a small pilot install
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
@@ -626,21 +647,21 @@ pilot:
|
||||
cpu:
|
||||
targetAverageUtilization: 80
|
||||
|
||||
# if protocol sniffing is enabled for outbound
|
||||
# -- if protocol sniffing is enabled for outbound
|
||||
enableProtocolSniffingForOutbound: true
|
||||
# if protocol sniffing is enabled for inbound
|
||||
# -- if protocol sniffing is enabled for inbound
|
||||
enableProtocolSniffingForInbound: true
|
||||
|
||||
nodeSelector: {}
|
||||
podAnnotations: {}
|
||||
serviceAnnotations: {}
|
||||
|
||||
# You can use jwksResolverExtraRootCA to provide a root certificate
|
||||
# -- You can use jwksResolverExtraRootCA to provide a root certificate
|
||||
# in PEM format. This will then be trusted by pilot when resolving
|
||||
# JWKS URIs.
|
||||
jwksResolverExtraRootCA: ""
|
||||
|
||||
# This is used to set the source of configuration for
|
||||
# -- This is used to set the source of configuration for
|
||||
# the associated address in configSource, if nothing is specified
|
||||
# the default MCP is assumed.
|
||||
configSource:
|
||||
@@ -648,34 +669,48 @@ pilot:
|
||||
|
||||
plugins: []
|
||||
|
||||
# The following is used to limit how long a sidecar can be connected
|
||||
# -- The following is used to limit how long a sidecar can be connected
|
||||
# to a pilot. It balances out load across pilot instances at the cost of
|
||||
# increasing system churn.
|
||||
keepaliveMaxServerConnectionAge: 30m
|
||||
|
||||
# Additional labels to apply to the deployment.
|
||||
# -- Additional labels to apply to the deployment.
|
||||
deploymentLabels: {}
|
||||
|
||||
|
||||
## Mesh config settings
|
||||
|
||||
# Install the mesh config map, generated from values.yaml.
|
||||
# -- Install the mesh config map, generated from values.yaml.
|
||||
# If false, pilot wil use default values (by default) or user-supplied values.
|
||||
configMap: true
|
||||
|
||||
# Additional labels to apply on the pod level for monitoring and logging configuration.
|
||||
# -- 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
|
||||
|
||||
@@ -1,57 +1,276 @@
|
||||
# Higress Helm Chart
|
||||
|
||||
Installs the cloud-native gateway [Higress](http://higress.io/)
|
||||
|
||||
## Get Repo Info
|
||||
|
||||
```console
|
||||
helm repo add higress.io https://higress.io/helm-charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
_See [helm repo](https://helm.sh/docs/helm/helm_repo/) for command documentation._
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `higress`:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
```
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
To uninstall/delete the higress deployment:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## Configuration
|
||||
|
||||
| **Parameter** | **Description** | **Default** |
|
||||
|---|---|---|
|
||||
| **Global Parameters** | | |
|
||||
| global.local | Set to `true` if installing to a local K8s cluster (e.g.: Kind, Rancher Desktop, etc.) | false |
|
||||
| global.ingressClass | [IngressClass](https://kubernetes.io/zh-cn/docs/concepts/services-networking/ingress/#ingress-class) which is used to filter Ingress resources Higress Controller watches.<br />If there are multiple gateway instances deployed in the cluster, this parameter can be used to distinguish the scope of each gateway instance.<br />There are some special cases for special IngressClass values:<br />1. If set to "nginx", Higress Controller will watch Ingress resources with the `nginx` IngressClass or without any Ingress class.<br />2. If set to empty, Higress Controller will watch all Ingress resources in the K8s cluster. | higress |
|
||||
| global.watchNamespace | If not empty, Higress Controller will only watch resources in the specified namespace. When isolating different business systems using K8s namespace, if each namespace requires a standalone gateway instance, this parameter can be used to confine the Ingress watching of Higress within the given namespace. | "" |
|
||||
| global.disableAlpnH2 | Whether to disable HTTP/2 in ALPN | true |
|
||||
| global.enableStatus | If `true`, Higress Controller will update the `status` field of Ingress resources.<br />When migrating from Nginx Ingress, in order to avoid `status` field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the `status` field of the corresponding Ingress object. | true |
|
||||
| global.enableIstioAPI | If `true`, Higress Controller will monitor istio resources as well | false |
|
||||
| global.enableGatewayAPI | If `true`, Higress Controller will monitor Gateway API resources as well | false |
|
||||
| global.istioNamespace | The namespace istio is installed to | istio-system |
|
||||
| **Core Paramters** | | |
|
||||
| higress-core.gateway.replicas | Number of Higress Gateway pods | 2 |
|
||||
| higress-core.controller.replicas | Number of Higress Controller pods | 1 |
|
||||
| **Console Paramters** | | |
|
||||
| higress-console.replicaCount | Number of Higress Console pods | 1 |
|
||||
| higress-console.service.type | K8s service type used by Higress Console | ClusterIP |
|
||||
| higress-console.domain | Domain used to access Higress Console | console.higress.io |
|
||||
| higress-console.tlsSecretName | Name of Secret resource used by TLS connections. | "" |
|
||||
| higress-console.web.login.prompt | Prompt message to be displayed on the login page | "" |
|
||||
| higress-console.admin.password.value | If not empty, the admin password will be configured to the specified value. | "" |
|
||||
| higress-console.admin.password.length | The length of random admin password generated during installation. Only works when `higress-console.admin.password.value` is not set. | 8 |
|
||||
| higress-console.o11y.enabled | If `true`, o11y suite (Grafana + Promethues) will be installed. | false |
|
||||
| higress-console.pvc.rwxSupported | Set to `false` when installing to a standard K8s cluster and the target cluster doesn't support the ReadWriteMany access mode of PersistentVolumeClaim. | true |
|
||||
## Higress for Kubernetes
|
||||
|
||||
Higress is a cloud-native api gateway based on Alibaba's internal gateway practices.
|
||||
|
||||
Powered by Istio and Envoy, Higress realizes the integration of the triple gateway architecture of traffic gateway, microservice gateway and security gateway, thereby greatly reducing the costs of deployment, operation and maintenance.
|
||||
|
||||
## Setup Repo Info
|
||||
|
||||
```console
|
||||
helm repo add higress.io https://higress.io/helm-charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
To install the chart with the release name `higress`:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
To uninstall/delete the higress deployment:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## Parameters
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| clusterName | string | `""` | |
|
||||
| controller.affinity | object | `{}` | |
|
||||
| controller.automaticHttps.email | string | `""` | |
|
||||
| controller.automaticHttps.enabled | bool | `true` | |
|
||||
| controller.autoscaling.enabled | bool | `false` | |
|
||||
| controller.autoscaling.maxReplicas | int | `5` | |
|
||||
| controller.autoscaling.minReplicas | int | `1` | |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| controller.env | object | `{}` | |
|
||||
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| controller.image | string | `"higress"` | |
|
||||
| controller.imagePullSecrets | list | `[]` | |
|
||||
| controller.labels | object | `{}` | |
|
||||
| controller.name | string | `"higress-controller"` | |
|
||||
| controller.nodeSelector | object | `{}` | |
|
||||
| controller.podAnnotations | object | `{}` | |
|
||||
| controller.podSecurityContext | object | `{}` | |
|
||||
| controller.ports[0].name | string | `"http"` | |
|
||||
| controller.ports[0].port | int | `8888` | |
|
||||
| controller.ports[0].protocol | string | `"TCP"` | |
|
||||
| controller.ports[0].targetPort | int | `8888` | |
|
||||
| controller.ports[1].name | string | `"http-solver"` | |
|
||||
| controller.ports[1].port | int | `8889` | |
|
||||
| controller.ports[1].protocol | string | `"TCP"` | |
|
||||
| controller.ports[1].targetPort | int | `8889` | |
|
||||
| controller.ports[2].name | string | `"grpc"` | |
|
||||
| controller.ports[2].port | int | `15051` | |
|
||||
| controller.ports[2].protocol | string | `"TCP"` | |
|
||||
| controller.ports[2].targetPort | int | `15051` | |
|
||||
| controller.probe.httpGet.path | string | `"/ready"` | |
|
||||
| controller.probe.httpGet.port | int | `8888` | |
|
||||
| controller.probe.initialDelaySeconds | int | `1` | |
|
||||
| controller.probe.periodSeconds | int | `3` | |
|
||||
| controller.probe.timeoutSeconds | int | `5` | |
|
||||
| controller.rbac.create | bool | `true` | |
|
||||
| controller.replicas | int | `1` | Number of Higress Controller pods |
|
||||
| controller.resources.limits.cpu | string | `"1000m"` | |
|
||||
| controller.resources.limits.memory | string | `"2048Mi"` | |
|
||||
| controller.resources.requests.cpu | string | `"500m"` | |
|
||||
| controller.resources.requests.memory | string | `"2048Mi"` | |
|
||||
| controller.securityContext | object | `{}` | |
|
||||
| controller.service.type | string | `"ClusterIP"` | |
|
||||
| controller.serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| controller.serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| controller.serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
|
||||
| controller.tag | string | `""` | |
|
||||
| controller.tolerations | list | `[]` | |
|
||||
| downstream | object | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | Downstream config settings |
|
||||
| gateway.affinity | object | `{}` | |
|
||||
| gateway.annotations | object | `{}` | Annotations to apply to all resources |
|
||||
| gateway.autoscaling.enabled | bool | `false` | |
|
||||
| gateway.autoscaling.maxReplicas | int | `5` | |
|
||||
| gateway.autoscaling.minReplicas | int | `1` | |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| gateway.containerSecurityContext | string | `nil` | |
|
||||
| gateway.env | object | `{}` | Pod environment variables |
|
||||
| gateway.hostNetwork | bool | `false` | |
|
||||
| gateway.httpPort | int | `80` | |
|
||||
| gateway.httpsPort | int | `443` | |
|
||||
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| gateway.image | string | `"gateway"` | |
|
||||
| gateway.kind | string | `"Deployment"` | Use a `DaemonSet` or `Deployment` |
|
||||
| gateway.labels | object | `{}` | Labels to apply to all resources |
|
||||
| gateway.metrics.enabled | bool | `false` | If true, create PodMonitor or VMPodScrape for gateway |
|
||||
| gateway.metrics.honorLabels | bool | `false` | |
|
||||
| gateway.metrics.interval | string | `""` | |
|
||||
| gateway.metrics.metricRelabelConfigs | list | `[]` | for operator.victoriametrics.com/v1beta1.VMPodScrape |
|
||||
| gateway.metrics.metricRelabelings | list | `[]` | for monitoring.coreos.com/v1.PodMonitor |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | provider group name for CustomResourceDefinition, can be monitoring.coreos.com or operator.victoriametrics.com |
|
||||
| gateway.metrics.rawSpec | object | `{}` | some more raw podMetricsEndpoints spec |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | |
|
||||
| gateway.metrics.relabelings | list | `[]` | |
|
||||
| gateway.metrics.scrapeTimeout | string | `""` | |
|
||||
| gateway.name | string | `"higress-gateway"` | |
|
||||
| gateway.networkGateway | string | `""` | If specified, the gateway will act as a network gateway for the given network. |
|
||||
| gateway.nodeSelector | object | `{}` | |
|
||||
| gateway.podAnnotations."prometheus.io/path" | string | `"/stats/prometheus"` | |
|
||||
| gateway.podAnnotations."prometheus.io/port" | string | `"15020"` | |
|
||||
| gateway.podAnnotations."prometheus.io/scrape" | string | `"true"` | |
|
||||
| gateway.podAnnotations."sidecar.istio.io/inject" | string | `"false"` | |
|
||||
| gateway.rbac.enabled | bool | `true` | If enabled, roles will be created to enable accessing certificates from Gateways. This is not needed when using http://gateway-api.org/. |
|
||||
| gateway.readinessFailureThreshold | int | `30` | The number of successive failed probes before indicating readiness failure. |
|
||||
| gateway.readinessInitialDelaySeconds | int | `1` | The initial delay for readiness probes in seconds. |
|
||||
| gateway.readinessPeriodSeconds | int | `2` | The period between readiness probes. |
|
||||
| gateway.readinessSuccessThreshold | int | `1` | The number of successive successed probes before indicating readiness success. |
|
||||
| gateway.readinessTimeoutSeconds | int | `3` | The readiness timeout seconds |
|
||||
| gateway.replicas | int | `2` | Number of Higress Gateway pods |
|
||||
| gateway.resources.limits.cpu | string | `"2000m"` | |
|
||||
| gateway.resources.limits.memory | string | `"2048Mi"` | |
|
||||
| gateway.resources.requests.cpu | string | `"2000m"` | |
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | |
|
||||
| gateway.revision | string | `""` | revision declares which revision this gateway is a part of |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | |
|
||||
| gateway.securityContext | string | `nil` | 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. |
|
||||
| gateway.service.annotations | object | `{}` | |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | |
|
||||
| gateway.service.loadBalancerClass | string | `""` | |
|
||||
| gateway.service.loadBalancerIP | string | `""` | |
|
||||
| gateway.service.loadBalancerSourceRanges | list | `[]` | |
|
||||
| gateway.service.ports[0].name | string | `"http2"` | |
|
||||
| gateway.service.ports[0].port | int | `80` | |
|
||||
| gateway.service.ports[0].protocol | string | `"TCP"` | |
|
||||
| gateway.service.ports[0].targetPort | int | `80` | |
|
||||
| gateway.service.ports[1].name | string | `"https"` | |
|
||||
| gateway.service.ports[1].port | int | `443` | |
|
||||
| gateway.service.ports[1].protocol | string | `"TCP"` | |
|
||||
| gateway.service.ports[1].targetPort | int | `443` | |
|
||||
| gateway.service.type | string | `"LoadBalancer"` | Type of service. Set to "None" to disable the service entirely |
|
||||
| gateway.serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| gateway.serviceAccount.create | bool | `true` | If set, a service account will be created. Otherwise, the default is used |
|
||||
| gateway.serviceAccount.name | string | `""` | The name of the service account to use. If not set, the release name is used |
|
||||
| gateway.tag | string | `""` | |
|
||||
| gateway.tolerations | list | `[]` | |
|
||||
| global.autoscalingv2API | bool | `true` | whether to use autoscaling/v2 template for HPA settings for internal usage only, not to be configured by users. |
|
||||
| global.caAddress | string | `""` | The customized CA address to retrieve certificates for the pods in the cluster. CSR clients such as the Istio Agent and ingress gateways can use this to specify the CA endpoint. If not set explicitly, default to the Istio discovery address. |
|
||||
| global.caName | string | `""` | The name of the CA for workload certificates. For example, when caName=GkeWorkloadCertificate, GKE workload certificates will be used as the certificates for workloads. The default value is "" and when caName="", the CA will be configured by other mechanisms (e.g., environmental variable CA_PROVIDER). |
|
||||
| global.configCluster | bool | `false` | Configure a remote cluster as the config cluster for an external istiod. |
|
||||
| global.defaultPodDisruptionBudget | object | `{"enabled":false}` | enable pod disruption budget for the control plane, which is used to ensure Istio control plane components are gradually upgraded or recovered. |
|
||||
| global.defaultResources | object | `{"requests":{"cpu":"10m"}}` | A minimal set of requested resources to applied to all deployments so that Horizontal Pod Autoscaler will be able to function (if set). Each component can overwrite these default values by adding its own resources block in the relevant section below and setting the desired resources values. |
|
||||
| global.defaultUpstreamConcurrencyThreshold | int | `10000` | |
|
||||
| global.disableAlpnH2 | bool | `false` | Whether to disable HTTP/2 in ALPN |
|
||||
| global.enableGatewayAPI | bool | `false` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableH3 | bool | `false` | |
|
||||
| global.enableHigressIstio | bool | `false` | |
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableProxyProtocol | bool | `false` | |
|
||||
| global.enableSRDS | bool | `true` | |
|
||||
| global.enableStatus | bool | `true` | If true, Higress Controller will update the status field of Ingress resources. When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the status field of the corresponding Ingress object. |
|
||||
| global.externalIstiod | bool | `false` | Configure a remote cluster data plane controlled by an external istiod. When set to true, istiod is not deployed locally and only a subset of the other discovery charts are enabled. |
|
||||
| global.hostRDSMergeSubset | bool | `false` | |
|
||||
| global.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | Default hub for Istio images. Releases are published to docker hub under 'istio' project. Dev builds from prow are on gcr.io |
|
||||
| global.imagePullPolicy | string | `""` | Specify image pull policy if default behavior isn't desired. Default behavior: latest images will be Always else IfNotPresent. |
|
||||
| global.imagePullSecrets | list | `[]` | ImagePullSecrets for all ServiceAccount, list of secrets in the same namespace to use for pulling any images in pods that reference this ServiceAccount. For components that don't use ServiceAccounts (i.e. grafana, servicegraph, tracing) ImagePullSecrets will be added to the corresponding Deployment(StatefulSet) objects. Must be set for any cluster configured with private docker registry. |
|
||||
| global.ingressClass | string | `"higress"` | IngressClass filters which ingress resources the higress controller watches. The default ingress class is higress. There are some special cases for special ingress class. 1. When the ingress class is set as nginx, the higress controller will watch ingress resources with the nginx ingress class or without any ingress class. 2. When the ingress class is set empty, the higress controller will watch all ingress resources in the k8s cluster. |
|
||||
| global.istioNamespace | string | `"istio-system"` | Used to locate istiod. |
|
||||
| global.istiod | object | `{"enableAnalysis":false}` | Enabled by default in master for maximising testing. |
|
||||
| global.jwtPolicy | string | `"third-party-jwt"` | Configure the policy for validating JWT. Currently, two options are supported: "third-party-jwt" and "first-party-jwt". |
|
||||
| global.kind | bool | `false` | |
|
||||
| global.liteMetrics | bool | `true` | |
|
||||
| global.local | bool | `false` | When deploying to a local cluster (e.g.: kind cluster), set this to true. |
|
||||
| global.logAsJson | bool | `false` | |
|
||||
| global.logging | object | `{"level":"default:info"}` | Comma-separated minimum per-scope logging level of messages to output, in the form of <scope>:<level>,<scope>:<level> The control plane has different scopes depending on component, but can configure default log level across all components If empty, default scope and level will be used as configured in code |
|
||||
| global.meshID | string | `""` | If the mesh admin does not specify a value, Istio will use the value of the mesh's Trust Domain. The best practice is to select a proper Trust Domain value. |
|
||||
| global.meshNetworks | object | `{}` | |
|
||||
| global.mountMtlsCerts | bool | `false` | Use the user-specified, secret volume mounted key and certs for Pilot and workloads. |
|
||||
| global.multiCluster.clusterName | string | `""` | Should be set to the name of the cluster this installation will run in. This is required for sidecar injection to properly label proxies |
|
||||
| global.multiCluster.enabled | bool | `true` | Set to true to connect two kubernetes clusters via their respective ingressgateway services when pods in each cluster cannot directly talk to one another. All clusters should be using Istio mTLS and must have a shared root CA for this model to work. |
|
||||
| global.network | string | `""` | Network defines the network this cluster belong to. This name corresponds to the networks in the map of mesh networks. |
|
||||
| global.o11y | object | `{"enabled":false,"promtail":{"image":{"repository":"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/promtail","tag":"2.9.4"},"port":3101,"resources":{"limits":{"cpu":"500m","memory":"2Gi"}},"securityContext":{}}}` | Observability (o11y) configurations |
|
||||
| global.omitSidecarInjectorConfigMap | bool | `false` | |
|
||||
| global.onDemandRDS | bool | `false` | |
|
||||
| global.oneNamespace | bool | `false` | Whether to restrict the applications namespace the controller manages; If not set, controller watches all namespaces |
|
||||
| global.onlyPushRouteCluster | bool | `true` | |
|
||||
| global.operatorManageWebhooks | bool | `false` | Configure whether Operator manages webhook configurations. The current behavior of Istiod is to manage its own webhook configurations. When this option is set as true, Istio Operator, instead of webhooks, manages the webhook configurations. When this option is set as false, webhooks manage their own webhook configurations. |
|
||||
| global.pilotCertProvider | string | `"istiod"` | Configure the certificate provider for control plane communication. Currently, two providers are supported: "kubernetes" and "istiod". As some platforms may not have kubernetes signing APIs, Istiod is the default |
|
||||
| global.priorityClassName | string | `""` | Kubernetes >=v1.11.0 will create two PriorityClass, including system-cluster-critical and system-node-critical, it is better to configure this in order to make sure your Istio pods will not be killed because of low priority class. Refer to https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass for more detail. |
|
||||
| global.proxy.autoInject | string | `"enabled"` | This controls the 'policy' in the sidecar injector. |
|
||||
| global.proxy.clusterDomain | string | `"cluster.local"` | CAUTION: It is important to ensure that all Istio helm charts specify the same clusterDomain value cluster domain. Default value is "cluster.local". |
|
||||
| global.proxy.componentLogLevel | string | `"misc:error"` | Per Component log level for proxy, applies to gateways and sidecars. If a component level is not set, then the global "logLevel" will be used. |
|
||||
| global.proxy.enableCoreDump | bool | `false` | If set, newly injected sidecars will have core dumps enabled. |
|
||||
| global.proxy.excludeIPRanges | string | `""` | |
|
||||
| global.proxy.excludeInboundPorts | string | `""` | |
|
||||
| global.proxy.excludeOutboundPorts | string | `""` | |
|
||||
| global.proxy.holdApplicationUntilProxyStarts | bool | `false` | Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready |
|
||||
| global.proxy.image | string | `"proxyv2"` | |
|
||||
| global.proxy.includeIPRanges | string | `"*"` | 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" would only capture egress traffic on those two IP Ranges, all other outbound traffic would be allowed by the sidecar |
|
||||
| global.proxy.includeInboundPorts | string | `"*"` | |
|
||||
| global.proxy.includeOutboundPorts | string | `""` | |
|
||||
| global.proxy.logLevel | string | `"warning"` | Log level for proxy, applies to gateways and sidecars. Expected values are: trace|debug|info|warning|error|critical|off |
|
||||
| global.proxy.privileged | bool | `false` | If set to true, istio-proxy container will have privileged securityContext |
|
||||
| global.proxy.readinessFailureThreshold | int | `30` | The number of successive failed probes before indicating readiness failure. |
|
||||
| global.proxy.readinessInitialDelaySeconds | int | `1` | The initial delay for readiness probes in seconds. |
|
||||
| global.proxy.readinessPeriodSeconds | int | `2` | The period between readiness probes. |
|
||||
| global.proxy.readinessSuccessThreshold | int | `30` | The number of successive successed probes before indicating readiness success. |
|
||||
| global.proxy.readinessTimeoutSeconds | int | `3` | The readiness timeout seconds |
|
||||
| global.proxy.resources | object | `{"limits":{"cpu":"2000m","memory":"1024Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}` | Resources for the sidecar. |
|
||||
| global.proxy.statusPort | int | `15020` | Default port for Pilot agent health checks. A value of 0 will disable health checking. |
|
||||
| global.proxy.tracer | string | `""` | Specify which tracer to use. One of: lightstep, datadog, stackdriver. If using stackdriver tracer outside GCP, set env GOOGLE_APPLICATION_CREDENTIALS to the GCP credential file. |
|
||||
| global.proxy_init.image | string | `"proxyv2"` | Base name for the proxy_init container, used to configure iptables. |
|
||||
| global.proxy_init.resources.limits.cpu | string | `"2000m"` | |
|
||||
| global.proxy_init.resources.limits.memory | string | `"1024Mi"` | |
|
||||
| global.proxy_init.resources.requests.cpu | string | `"10m"` | |
|
||||
| global.proxy_init.resources.requests.memory | string | `"10Mi"` | |
|
||||
| global.remotePilotAddress | string | `""` | configure remote pilot and istiod service and endpoint |
|
||||
| global.sds.token | object | `{"aud":"istio-ca"}` | The JWT token for SDS and the aud field of such JWT. See RFC 7519, section 4.1.3. When a CSR is sent from Istio Agent to the CA (e.g. Istiod), this aud is to make sure the JWT is intended for the CA. |
|
||||
| global.sts.servicePort | int | `0` | The service port used by Security Token Service (STS) server to handle token exchange requests. Setting this port to a non-zero value enables STS server. |
|
||||
| global.tracer | object | `{"datadog":{"address":"$(HOST_IP):8126"},"lightstep":{"accessToken":"","address":""},"stackdriver":{"debug":false,"maxNumberOfAnnotations":200,"maxNumberOfAttributes":200,"maxNumberOfMessageEvents":200}}` | Configuration for each of the supported tracers |
|
||||
| global.tracer.datadog | object | `{"address":"$(HOST_IP):8126"}` | Configuration for envoy to send trace data to LightStep. Disabled by default. address: the <host>:<port> of the satellite pool accessToken: required for sending data to the pool |
|
||||
| global.tracer.datadog.address | string | `"$(HOST_IP):8126"` | Host:Port for submitting traces to the Datadog agent. |
|
||||
| global.tracer.lightstep.accessToken | string | `""` | example: abcdefg1234567 |
|
||||
| global.tracer.lightstep.address | string | `""` | example: lightstep-satellite:443 |
|
||||
| global.tracer.stackdriver.debug | bool | `false` | enables trace output to stdout. |
|
||||
| global.tracer.stackdriver.maxNumberOfAnnotations | int | `200` | The global default max number of annotation events per span. |
|
||||
| global.tracer.stackdriver.maxNumberOfAttributes | int | `200` | The global default max number of attributes per span. |
|
||||
| global.tracer.stackdriver.maxNumberOfMessageEvents | int | `200` | The global default max number of message events per span. |
|
||||
| global.useMCP | bool | `false` | Use the Mesh Control Protocol (MCP) for configuring Istiod. Requires an MCP source. |
|
||||
| global.watchNamespace | string | `""` | If not empty, Higress Controller will only watch resources in the specified namespace. When isolating different business systems using K8s namespace, if each namespace requires a standalone gateway instance, this parameter can be used to confine the Ingress watching of Higress within the given namespace. |
|
||||
| global.xdsMaxRecvMsgSize | string | `"104857600"` | |
|
||||
| hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| meshConfig | object | `{"enablePrometheusMerge":true,"rootNamespace":null,"trustDomain":"cluster.local"}` | meshConfig defines runtime configuration of components, including Istiod and istio-agent behavior See https://istio.io/docs/reference/config/istio.mesh.v1alpha1/ for all available options |
|
||||
| meshConfig.rootNamespace | string | `nil` | The namespace to treat as the administrative root namespace for Istio configuration. When processing a leaf namespace Istio will search for declarations in that namespace first and if none are found it will search in the root namespace. Any matching declaration found in the root namespace is processed as if it were declared in the leaf namespace. |
|
||||
| meshConfig.trustDomain | string | `"cluster.local"` | The trust domain corresponds to the trust root of a system Refer to https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md#21-trust-domain |
|
||||
| pilot.autoscaleEnabled | bool | `false` | |
|
||||
| pilot.autoscaleMax | int | `5` | |
|
||||
| pilot.autoscaleMin | int | `1` | |
|
||||
| pilot.configMap | bool | `true` | Install the mesh config map, generated from values.yaml. If false, pilot wil use default values (by default) or user-supplied values. |
|
||||
| pilot.configSource | object | `{"subscribedResources":[]}` | This is used to set the source of configuration for the associated address in configSource, if nothing is specified the default MCP is assumed. |
|
||||
| pilot.cpu.targetAverageUtilization | int | `80` | |
|
||||
| pilot.deploymentLabels | object | `{}` | Additional labels to apply to the deployment. |
|
||||
| pilot.enableProtocolSniffingForInbound | bool | `true` | if protocol sniffing is enabled for inbound |
|
||||
| pilot.enableProtocolSniffingForOutbound | bool | `true` | if protocol sniffing is enabled for outbound |
|
||||
| pilot.env.PILOT_ENABLE_CROSS_CLUSTER_WORKLOAD_ENTRY | string | `"false"` | |
|
||||
| pilot.env.PILOT_ENABLE_METADATA_EXCHANGE | string | `"false"` | |
|
||||
| pilot.env.PILOT_SCOPE_GATEWAY_TO_NAMESPACE | string | `"false"` | |
|
||||
| pilot.env.VALIDATION_ENABLED | string | `"false"` | |
|
||||
| pilot.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| pilot.image | string | `"pilot"` | Can be a full hub/image:tag |
|
||||
| pilot.jwksResolverExtraRootCA | string | `""` | You can use jwksResolverExtraRootCA to provide a root certificate in PEM format. This will then be trusted by pilot when resolving JWKS URIs. |
|
||||
| pilot.keepaliveMaxServerConnectionAge | string | `"30m"` | The following is used to limit how long a sidecar can be connected to a pilot. It balances out load across pilot instances at the cost of increasing system churn. |
|
||||
| pilot.nodeSelector | object | `{}` | |
|
||||
| pilot.plugins | list | `[]` | |
|
||||
| pilot.podAnnotations | object | `{}` | |
|
||||
| pilot.podLabels | object | `{}` | Additional labels to apply on the pod level for monitoring and logging configuration. |
|
||||
| pilot.replicaCount | int | `1` | |
|
||||
| pilot.resources | object | `{"requests":{"cpu":"500m","memory":"2048Mi"}}` | Resources for a small pilot install |
|
||||
| pilot.rollingMaxSurge | string | `"100%"` | |
|
||||
| pilot.rollingMaxUnavailable | string | `"25%"` | |
|
||||
| pilot.serviceAnnotations | object | `{}` | |
|
||||
| pilot.tag | string | `""` | |
|
||||
| pilot.traceSampling | float | `1` | |
|
||||
| revision | string | `""` | |
|
||||
| tracing.enable | bool | `false` | |
|
||||
| tracing.sampling | int | `100` | |
|
||||
| tracing.skywalking.port | int | `11800` | |
|
||||
| tracing.skywalking.service | string | `""` | |
|
||||
| tracing.timeout | int | `500` | |
|
||||
| upstream | object | `{"connectionBufferLimits":10485760,"idleTimeout":10}` | Upstream config settings |
|
||||
34
helm/higress/README.md.gotmpl
Normal file
34
helm/higress/README.md.gotmpl
Normal file
@@ -0,0 +1,34 @@
|
||||
## Higress for Kubernetes
|
||||
|
||||
Higress is a cloud-native api gateway based on Alibaba's internal gateway practices.
|
||||
|
||||
Powered by Istio and Envoy, Higress realizes the integration of the triple gateway architecture of traffic gateway, microservice gateway and security gateway, thereby greatly reducing the costs of deployment, operation and maintenance.
|
||||
|
||||
## Setup Repo Info
|
||||
|
||||
```console
|
||||
helm repo add higress.io https://higress.io/helm-charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
To install the chart with the release name `higress`:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
To uninstall/delete the higress deployment:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## Parameters
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
||||
@@ -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...1dbd773596
@@ -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,24 +16,28 @@ 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")
|
||||
load("@proxy_wasm_cpp_sdk//bazel:repositories.bzl", "proxy_wasm_cpp_sdk_repositories")
|
||||
|
||||
wasm_dependencies()
|
||||
proxy_wasm_cpp_sdk_repositories()
|
||||
|
||||
load("@proxy_wasm_cpp_sdk//bazel/dep:deps_extra.bzl", "wasm_dependencies_extra")
|
||||
load("@proxy_wasm_cpp_sdk//bazel:dependencies.bzl", "proxy_wasm_cpp_sdk_dependencies")
|
||||
|
||||
wasm_dependencies_extra()
|
||||
proxy_wasm_cpp_sdk_dependencies()
|
||||
|
||||
load("@proxy_wasm_cpp_sdk//bazel:dependencies_extra.bzl", "proxy_wasm_cpp_sdk_dependencies_extra")
|
||||
|
||||
proxy_wasm_cpp_sdk_dependencies_extra()
|
||||
|
||||
load("@istio_ecosystem_wasm_extensions//bazel:wasm.bzl", "wasm_libraries")
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@ diff --git a/absl/time/internal/cctz/src/time_zone_format.cc b/absl/time/interna
|
||||
index d8cb047..0c5f182 100644
|
||||
--- a/absl/time/internal/cctz/src/time_zone_format.cc
|
||||
+++ b/absl/time/internal/cctz/src/time_zone_format.cc
|
||||
@@ -18,6 +18,8 @@
|
||||
#endif
|
||||
#endif
|
||||
@@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
+#define HAS_STRPTIME 0
|
||||
+
|
||||
#if defined(HAS_STRPTIME) && HAS_STRPTIME
|
||||
#if !defined(_XOPEN_SOURCE)
|
||||
#define _XOPEN_SOURCE // Definedness suffices for strptime.
|
||||
@@ -58,7 +60,7 @@ namespace {
|
||||
#if !defined(HAS_STRPTIME)
|
||||
#if !defined(_MSC_VER) && !defined(__MINGW32__)
|
||||
#define HAS_STRPTIME 1 // assume everyone has strptime() except windows
|
||||
@@ -58,7 +60,7 @@
|
||||
|
||||
#if !HAS_STRPTIME
|
||||
// Build a strptime() using C++11's std::get_time().
|
||||
@@ -20,7 +20,7 @@ index d8cb047..0c5f182 100644
|
||||
std::istringstream input(s);
|
||||
input >> std::get_time(tm, fmt);
|
||||
if (input.fail()) return nullptr;
|
||||
@@ -648,7 +650,7 @@ const char* ParseSubSeconds(const char* dp, detail::femtoseconds* subseconds) {
|
||||
@@ -648,7 +650,7 @@
|
||||
// Parses a string into a std::tm using strptime(3).
|
||||
const char* ParseTM(const char* dp, const char* fmt, std::tm* tm) {
|
||||
if (dp != nullptr) {
|
||||
|
||||
@@ -9,9 +9,9 @@ load(
|
||||
def wasm_libraries():
|
||||
http_archive(
|
||||
name = "com_google_absl",
|
||||
sha256 = "ec8ef47335310cc3382bdc0d0cc1097a001e67dc83fcba807845aa5696e7e1e4",
|
||||
strip_prefix = "abseil-cpp-302b250e1d917ede77b5ff00a6fd9f28430f1563",
|
||||
url = "https://github.com/abseil/abseil-cpp/archive/302b250e1d917ede77b5ff00a6fd9f28430f1563.tar.gz",
|
||||
sha256 = "3a0bb3d2e6f53352526a8d1a7e7b5749c68cd07f2401766a404fb00d2853fa49",
|
||||
strip_prefix = "abseil-cpp-4bbdb026899fea9f882a95cbd7d6a4adaf49b2dd",
|
||||
url = "https://github.com/abseil/abseil-cpp/archive/4bbdb026899fea9f882a95cbd7d6a4adaf49b2dd.tar.gz",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//bazel:absl.patch"],
|
||||
)
|
||||
@@ -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 = "ecf42a27fcf78f42e64037d4eff1a0ca5a61e403"
|
||||
PROXY_WASM_CPP_HOST_SHA256 = "9748156731e9521837686923321bf12725c32c9fa8355218209831cc3ee87080"
|
||||
|
||||
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(
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
|
||||
#include "common/common_util.h"
|
||||
|
||||
namespace Wasm::Common::Http {
|
||||
@@ -190,7 +189,8 @@ std::vector<std::string> getAllOfHeader(std::string_view key) {
|
||||
std::vector<std::string> result;
|
||||
auto headers = getRequestHeaderPairs()->pairs();
|
||||
for (auto& header : headers) {
|
||||
if (absl::EqualsIgnoreCase(Wasm::Common::stdToAbsl(header.first), Wasm::Common::stdToAbsl(key))) {
|
||||
if (absl::EqualsIgnoreCase(Wasm::Common::stdToAbsl(header.first),
|
||||
Wasm::Common::stdToAbsl(key))) {
|
||||
result.push_back(std::string(header.second));
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,8 @@ void forEachCookie(
|
||||
v = v.substr(1, v.size() - 2);
|
||||
}
|
||||
|
||||
if (!cookie_consumer(Wasm::Common::abslToStd(k), Wasm::Common::abslToStd(v))) {
|
||||
if (!cookie_consumer(Wasm::Common::abslToStd(k),
|
||||
Wasm::Common::abslToStd(v))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +266,63 @@ std::string buildOriginalUri(std::optional<uint32_t> max_path_length) {
|
||||
auto scheme = scheme_ptr->view();
|
||||
auto host_ptr = getRequestHeader(Header::Host);
|
||||
auto host = host_ptr->view();
|
||||
return absl::StrCat(Wasm::Common::stdToAbsl(scheme), "://", Wasm::Common::stdToAbsl(host), Wasm::Common::stdToAbsl(final_path));
|
||||
return absl::StrCat(Wasm::Common::stdToAbsl(scheme), "://",
|
||||
Wasm::Common::stdToAbsl(host),
|
||||
Wasm::Common::stdToAbsl(final_path));
|
||||
}
|
||||
|
||||
void extractHostPathFromUri(const absl::string_view& uri,
|
||||
absl::string_view& host, absl::string_view& path) {
|
||||
/**
|
||||
* URI RFC: https://www.ietf.org/rfc/rfc2396.txt
|
||||
*
|
||||
* Example:
|
||||
* uri = "https://example.com:8443/certs"
|
||||
* pos: ^
|
||||
* host_pos: ^
|
||||
* path_pos: ^
|
||||
* host = "example.com:8443"
|
||||
* path = "/certs"
|
||||
*/
|
||||
const auto pos = uri.find("://");
|
||||
// Start position of the host
|
||||
const auto host_pos = (pos == std::string::npos) ? 0 : pos + 3;
|
||||
// Start position of the path
|
||||
const auto path_pos = uri.find('/', host_pos);
|
||||
if (path_pos == std::string::npos) {
|
||||
// If uri doesn't have "/", the whole string is treated as host.
|
||||
host = uri.substr(host_pos);
|
||||
path = "/";
|
||||
} else {
|
||||
host = uri.substr(host_pos, path_pos - host_pos);
|
||||
path = uri.substr(path_pos);
|
||||
}
|
||||
}
|
||||
|
||||
void extractPathWithoutArgsFromUri(const std::string_view& uri,
|
||||
std::string_view& path_without_args) {
|
||||
auto params_pos = uri.find('?');
|
||||
size_t uri_end;
|
||||
if (params_pos == std::string::npos) {
|
||||
uri_end = uri.size();
|
||||
} else {
|
||||
uri_end = params_pos;
|
||||
}
|
||||
path_without_args = uri.substr(0, uri_end);
|
||||
}
|
||||
|
||||
bool hasRequestBody() {
|
||||
auto contentType = getRequestHeader("content-type")->toString();
|
||||
auto contentLengthStr = getRequestHeader("content-length")->toString();
|
||||
auto transferEncoding = getRequestHeader("transfer-encoding")->toString();
|
||||
|
||||
if (!contentType.empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!contentLengthStr.empty()) {
|
||||
return true;
|
||||
}
|
||||
return transferEncoding.find("chunked") != std::string::npos;
|
||||
}
|
||||
|
||||
} // namespace Wasm::Common::Http
|
||||
|
||||
@@ -42,6 +42,12 @@ namespace Wasm::Common::Http {
|
||||
using QueryParams = std::map<std::string, std::string>;
|
||||
using SystemTime = std::chrono::time_point<std::chrono::system_clock>;
|
||||
|
||||
namespace Status {
|
||||
constexpr int OK = 200;
|
||||
constexpr int InternalServerError = 500;
|
||||
constexpr int Unauthorized = 401;
|
||||
} // namespace Status
|
||||
|
||||
namespace Header {
|
||||
constexpr std::string_view Scheme(":scheme");
|
||||
constexpr std::string_view Method(":method");
|
||||
@@ -52,14 +58,17 @@ constexpr std::string_view Accept("accept");
|
||||
constexpr std::string_view ContentMD5("content-md5");
|
||||
constexpr std::string_view ContentType("content-type");
|
||||
constexpr std::string_view ContentLength("content-length");
|
||||
constexpr std::string_view TransferEncoding("transfer-encoding");
|
||||
constexpr std::string_view UserAgent("user-agent");
|
||||
constexpr std::string_view Date("date");
|
||||
constexpr std::string_view Cookie("cookie");
|
||||
constexpr std::string_view StrictTransportSecurity("strict-transport-security");
|
||||
} // namespace Header
|
||||
|
||||
namespace ContentTypeValues {
|
||||
constexpr std::string_view Grpc{"application/grpc"};
|
||||
}
|
||||
constexpr std::string_view Json{"application/json"};
|
||||
} // namespace ContentTypeValues
|
||||
|
||||
class PercentEncoding {
|
||||
public:
|
||||
@@ -142,4 +151,10 @@ std::unordered_map<std::string, std::string> parseCookies(
|
||||
|
||||
std::string buildOriginalUri(std::optional<uint32_t> max_path_length);
|
||||
|
||||
void extractHostPathFromUri(const absl::string_view& uri,
|
||||
absl::string_view& host, absl::string_view& path);
|
||||
|
||||
void extractPathWithoutArgsFromUri(const std::string_view& uri,
|
||||
std::string_view& path_without_args);
|
||||
bool hasRequestBody();
|
||||
} // namespace Wasm::Common::Http
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "bot_detect.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -28,7 +28,6 @@ wasm_cc_binary(
|
||||
"//common:http_util",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "custom_response.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -27,7 +27,6 @@ wasm_cc_binary(
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "hmac_auth.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -30,7 +30,6 @@ wasm_cc_binary(
|
||||
"//common:crypto_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
@@ -33,7 +33,6 @@ wasm_cc_binary(
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "key_auth.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -28,7 +28,6 @@ wasm_cc_binary(
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "key_rate_limit.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -29,7 +29,6 @@ wasm_cc_binary(
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
70
plugins/wasm-cpp/extensions/model_mapper/BUILD
Normal file
70
plugins/wasm-cpp/extensions/model_mapper/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:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
proxy_wasm_cc_binary(
|
||||
name = "model_mapper.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_mapper_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_mapper_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":model_mapper_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "model_mapper",
|
||||
wasm_file = ":model_mapper.wasm",
|
||||
)
|
||||
63
plugins/wasm-cpp/extensions/model_mapper/README.md
Normal file
63
plugins/wasm-cpp/extensions/model_mapper/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
## 功能说明
|
||||
`model-mapper`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `modelMapping` | map of string | 选填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 ## 运行属性
|
||||
|
||||
插件执行阶段:认证阶段
|
||||
插件执行优先级:800
|
||||
|
|
||||
## 效果说明
|
||||
|
||||
如下配置
|
||||
|
||||
```yaml
|
||||
modelMapping:
|
||||
'gpt-4-*': "qwen-max"
|
||||
'gpt-4o': "qwen-vl-plus"
|
||||
'*': "qwen-turbo"
|
||||
```
|
||||
|
||||
开启后,`gpt-4-` 开头的模型参数会被改写为 `qwen-max`, `gpt-4o` 会被改写为 `qwen-vl-plus`,其他所有模型会被改写为 `qwen-turbo`
|
||||
|
||||
例如原本的请求是:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "higress项目主仓库的github地址是什么"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
经过这个插件后,原始的 LLM 请求体将被改成:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen-vl-plus",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "higress项目主仓库的github地址是什么"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
65
plugins/wasm-cpp/extensions/model_mapper/README_EN.md
Normal file
65
plugins/wasm-cpp/extensions/model_mapper/README_EN.md
Normal file
@@ -0,0 +1,65 @@
|
||||
## Function Description
|
||||
The `model-mapper` plugin implements the functionality of routing based on the model parameter in the LLM protocol.
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Name | Data Type | Filling Requirement | Default Value | Description |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | Optional | model | The location of the model parameter in the request body. |
|
||||
| `modelMapping` | map of string | Optional | - | AI model mapping table, used to map the model names in the request to the model names supported by the service provider.<br/>1. Supports prefix matching. For example, use "gpt-3-*" to match all models whose names start with “gpt-3-”;<br/>2. Supports using "*" as the key to configure a generic fallback mapping relationship;<br/>3. If the target name in the mapping is an empty string "", it means to keep the original model name. |
|
||||
| `enableOnPathSuffix` | array of string | Optional | ["/v1/chat/completions"] | Only applies to requests with these specific path suffixes. |
|
||||
|
||||
## Runtime Properties
|
||||
|
||||
Plugin execution phase: Authentication phase
|
||||
Plugin execution priority: 800
|
||||
|
||||
## Effect Description
|
||||
|
||||
With the following configuration:
|
||||
|
||||
```yaml
|
||||
modelMapping:
|
||||
'gpt-4-*': "qwen-max"
|
||||
'gpt-4o': "qwen-vl-plus"
|
||||
'*': "qwen-turbo"
|
||||
```
|
||||
|
||||
After enabling, model parameters starting with `gpt-4-` will be rewritten to `qwen-max`, `gpt-4o` will be rewritten to `qwen-vl-plus`, and all other models will be rewritten to `qwen-turbo`.
|
||||
|
||||
For example, if the original request was:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
After processing by this plugin, the original LLM request body will be modified to:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen-vl-plus",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
243
plugins/wasm-cpp/extensions/model_mapper/plugin.cc
Normal file
243
plugins/wasm-cpp/extensions/model_mapper/plugin.cc
Normal file
@@ -0,0 +1,243 @@
|
||||
// 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_mapper/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_mapper {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_ModelMapper(
|
||||
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,
|
||||
ModelMapperConfigRule& rule) {
|
||||
if (auto it = configuration.find("modelKey"); it != configuration.end()) {
|
||||
if (it->is_string()) {
|
||||
rule.model_key_ = it->get<std::string>();
|
||||
} else {
|
||||
LOG_ERROR("Invalid type for modelKey. Expected string.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto it = configuration.find("modelMapping"); it != configuration.end()) {
|
||||
if (!it->is_object()) {
|
||||
LOG_ERROR("Invalid type for modelMapping. Expected object.");
|
||||
return false;
|
||||
}
|
||||
auto model_mapping = it->get<Wasm::Common::JsonObject>();
|
||||
if (!JsonObjectIterate(model_mapping, [&](std::string key) -> bool {
|
||||
auto model_json = model_mapping.find(key);
|
||||
if (!model_json->is_string()) {
|
||||
LOG_ERROR(
|
||||
"Invalid type for item in modelMapping. Expected string.");
|
||||
return false;
|
||||
}
|
||||
if (key == "*") {
|
||||
rule.default_model_mapping_ = model_json->get<std::string>();
|
||||
return true;
|
||||
}
|
||||
if (absl::EndsWith(key, "*")) {
|
||||
rule.prefix_model_mapping_.emplace_back(
|
||||
absl::StripSuffix(key, "*"), model_json->get<std::string>());
|
||||
return true;
|
||||
}
|
||||
auto ret = rule.exact_model_mapping_.emplace(
|
||||
key, model_json->get<std::string>());
|
||||
if (!ret.second) {
|
||||
LOG_ERROR("Duplicate key in modelMapping: " + key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "enableOnPathSuffix", [&](const json& item) -> bool {
|
||||
if (item.is_string()) {
|
||||
rule.enable_on_path_suffix_.emplace_back(item.get<std::string>());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
LOG_WARN("Invalid type for item in enableOnPathSuffix. 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 ModelMapperConfigRule& rule) {
|
||||
if (!Wasm::Common::Http::hasRequestBody()) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto path = getRequestHeader(Wasm::Common::Http::Header::Path)->toString();
|
||||
auto params_pos = path.find('?');
|
||||
size_t uri_end;
|
||||
if (params_pos == std::string::npos) {
|
||||
uri_end = path.size();
|
||||
} else {
|
||||
uri_end = params_pos;
|
||||
}
|
||||
bool enable = false;
|
||||
for (const auto& enable_suffix : rule.enable_on_path_suffix_) {
|
||||
if (absl::EndsWith({path.c_str(), uri_end}, enable_suffix)) {
|
||||
enable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!enable) {
|
||||
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 ModelMapperConfigRule& rule,
|
||||
std::string_view body) {
|
||||
const auto& exact_model_mapping = rule.exact_model_mapping_;
|
||||
const auto& prefix_model_mapping = rule.prefix_model_mapping_;
|
||||
const auto& default_model_mapping = rule.default_model_mapping_;
|
||||
const auto& model_key = rule.model_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();
|
||||
std::string old_model;
|
||||
if (body_json.contains(model_key)) {
|
||||
old_model = body_json[model_key];
|
||||
}
|
||||
std::string model =
|
||||
default_model_mapping.empty() ? old_model : default_model_mapping;
|
||||
if (auto it = exact_model_mapping.find(old_model);
|
||||
it != exact_model_mapping.end()) {
|
||||
model = it->second;
|
||||
} else {
|
||||
for (auto& prefix_model_pair : prefix_model_mapping) {
|
||||
if (absl::StartsWith(old_model, prefix_model_pair.first)) {
|
||||
model = prefix_model_pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!model.empty() && model != old_model) {
|
||||
body_json[model_key] = model;
|
||||
setBuffer(WasmBufferType::HttpRequestBody, 0,
|
||||
std::numeric_limits<size_t>::max(), body_json.dump());
|
||||
LOG_DEBUG(
|
||||
absl::StrCat("model mapped, before:", old_model, ", after:", model));
|
||||
}
|
||||
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_mapper
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
87
plugins/wasm-cpp/extensions/model_mapper/plugin.h
Normal file
87
plugins/wasm-cpp/extensions/model_mapper/plugin.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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_mapper {
|
||||
|
||||
#endif
|
||||
|
||||
struct ModelMapperConfigRule {
|
||||
std::string model_key_ = "model";
|
||||
std::map<std::string, std::string> exact_model_mapping_;
|
||||
std::vector<std::pair<std::string, std::string>> prefix_model_mapping_;
|
||||
std::string default_model_mapping_;
|
||||
std::vector<std::string> enable_on_path_suffix_ = {"/v1/chat/completions"};
|
||||
};
|
||||
|
||||
// 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<ModelMapperConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
FilterHeadersStatus onHeader(const ModelMapperConfigRule&);
|
||||
FilterDataStatus onBody(const ModelMapperConfigRule&, std::string_view);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, ModelMapperConfigRule&) 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 ModelMapperConfigRule* config_ = nullptr;
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace model_mapper
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
301
plugins/wasm-cpp/extensions/model_mapper/plugin_test.cc
Normal file
301
plugins/wasm-cpp/extensions/model_mapper/plugin_test.cc
Normal file
@@ -0,0 +1,301 @@
|
||||
// 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_mapper/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_mapper {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_model_mapper_plugin("model_mapper", []() {
|
||||
return std::make_unique<NullPlugin>(model_mapper::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 ModelMapperTest : public ::testing::Test {
|
||||
protected:
|
||||
ModelMapperTest() {
|
||||
// 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_mapper");
|
||||
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";
|
||||
} else if (header == ":path") {
|
||||
*result = path_;
|
||||
}
|
||||
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) {
|
||||
if (absl::StartsWith(path, "route_name")) {
|
||||
*result = route_name_;
|
||||
} else if (absl::StartsWith(path, "cluster_name")) {
|
||||
*result = service_name_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_, setProperty(testing::_, testing::_))
|
||||
.WillByDefault(
|
||||
[&](std::string_view, std::string_view) { return WasmResult::Ok; });
|
||||
}
|
||||
~ModelMapperTest() 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_;
|
||||
std::string service_name_;
|
||||
std::string path_;
|
||||
BufferBase body_;
|
||||
BufferBase config_;
|
||||
};
|
||||
|
||||
TEST_F(ModelMapperTest, ModelMappingTest) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"modelMapping": {
|
||||
"*": "qwen-long",
|
||||
"gpt-4*": "qwen-max",
|
||||
"gpt-4o": "qwen-turbo",
|
||||
"gpt-4o-mini": "qwen-plus",
|
||||
"text-embedding-v1": ""
|
||||
}
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/v1/chat/completions";
|
||||
std::string request_json = R"({"model": "gpt-3.5"})";
|
||||
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;
|
||||
});
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
request_json = R"({"model": "gpt-4"})";
|
||||
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-max"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
request_json = R"({"model": "gpt-4o"})";
|
||||
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-turbo"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
request_json = R"({"model": "gpt-4o-mini"})";
|
||||
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-plus"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
request_json = R"({"model": "text-embedding-v1"})";
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.Times(0);
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
request_json = R"({})";
|
||||
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;
|
||||
});
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelMapperTest, RouteLevelModelMappingTest) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["route-a"],
|
||||
"_match_service_": ["service-1"],
|
||||
"modelMapping": {
|
||||
"*": "qwen-long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_match_route_": ["route-b"],
|
||||
"_match_service_": ["service-2"],
|
||||
"modelMapping": {
|
||||
"*": "qwen-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_match_route_": ["route-b"],
|
||||
"_match_service_": ["service-3"],
|
||||
"modelMapping": {
|
||||
"*": "qwen-turbo"
|
||||
}
|
||||
}
|
||||
]})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/api/v1/chat/completions";
|
||||
std::string request_json = R"({"model": "gpt-4"})";
|
||||
body_.set(request_json);
|
||||
route_name_ = "route-a";
|
||||
service_name_ = "outbound|80||service-1";
|
||||
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_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
route_name_ = "route-b";
|
||||
service_name_ = "outbound|80||service-2";
|
||||
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-max"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
|
||||
route_name_ = "route-b";
|
||||
service_name_ = "outbound|80||service-3";
|
||||
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-turbo"})");
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(20, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace model_mapper
|
||||
} // 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:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
proxy_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",
|
||||
)
|
||||
98
plugins/wasm-cpp/extensions/model_router/README.md
Normal file
98
plugins/wasm-cpp/extensions/model_router/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
## 功能说明
|
||||
`model-router`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
|
||||
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 |
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:认证阶段
|
||||
插件执行优先级:900
|
||||
|
||||
## 效果说明
|
||||
|
||||
### 基于 model 参数进行路由
|
||||
|
||||
需要做如下配置:
|
||||
|
||||
```yaml
|
||||
modelToHeader: x-higress-llm-model
|
||||
```
|
||||
|
||||
插件会将请求中 model 参数提取出来,设置到 x-higress-llm-model 这个请求 header 中,用于后续路由,举例来说,原生的 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
|
||||
}
|
||||
```
|
||||
|
||||
经过这个插件后,将添加下面这个请求头(可以用于路由匹配):
|
||||
|
||||
x-higress-llm-model: qwen-long
|
||||
|
||||
### 提取 model 参数中的 provider 字段用于路由
|
||||
|
||||
> 注意这种模式需要客户端在 model 参数中通过`/`分隔的方式,来指定 provider
|
||||
|
||||
需要做如下配置:
|
||||
|
||||
```yaml
|
||||
addProviderHeader: x-higress-llm-provider
|
||||
```
|
||||
|
||||
插件会将请求中 model 参数的 provider 部分(如果有)提取出来,设置到 x-higress-llm-provider 这个请求 header 中,用于后续路由,并将 model 参数重写为模型名称部分。举例来说,原生的 LLM 请求体是:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "dashscope/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: dashscope
|
||||
|
||||
原始的 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
|
||||
}
|
||||
```
|
||||
97
plugins/wasm-cpp/extensions/model_router/README_EN.md
Normal file
97
plugins/wasm-cpp/extensions/model_router/README_EN.md
Normal file
@@ -0,0 +1,97 @@
|
||||
## Function Description
|
||||
The `model-router` plugin implements the function of routing based on the model parameter in the LLM protocol.
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Name | Data Type | Filling Requirement | Default Value | Description |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | Optional | model | The location of the model parameter in the request body |
|
||||
| `addProviderHeader` | string | Optional | - | Which request header to place the provider name parsed from the model parameter |
|
||||
| `modelToHeader` | string | Optional | - | Which request header to directly place the model parameter |
|
||||
| `enableOnPathSuffix` | array of string | Optional | ["/v1/chat/completions"] | Only effective for requests with these specific path suffixes |
|
||||
|
||||
## Runtime Attributes
|
||||
|
||||
Plugin execution phase: Authentication phase
|
||||
Plugin execution priority: 900
|
||||
|
||||
## Effect Description
|
||||
|
||||
### Routing Based on the model Parameter
|
||||
|
||||
The following configuration is required:
|
||||
|
||||
```yaml
|
||||
modelToHeader: x-higress-llm-model
|
||||
```
|
||||
|
||||
The plugin will extract the model parameter from the request and set it in the x-higress-llm-model request header, which can be used for subsequent routing. For example, the original LLM request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen-long",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
After processing by this plugin, the following request header (which can be used for route matching) will be added:
|
||||
|
||||
x-higress-llm-model: qwen-long
|
||||
|
||||
### Extracting the provider Field from the model Parameter for Routing
|
||||
|
||||
> Note that this mode requires the client to specify the provider using a `/` separator in the model parameter.
|
||||
|
||||
The following configuration is required:
|
||||
|
||||
```yaml
|
||||
addProviderHeader: x-higress-llm-provider
|
||||
```
|
||||
|
||||
The plugin will extract the provider part (if present) from the model parameter in the request and set it in the x-higress-llm-provider request header, which can be used for subsequent routing, and rewrite the model parameter to the model name part. For example, the original LLM request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "dashscope/qwen-long",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
After processing by this plugin, the following request header (which can be used for route matching) will be added:
|
||||
|
||||
x-higress-llm-provider: dashscope
|
||||
|
||||
The original LLM request body will be changed to:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen-long",
|
||||
"frequency_penalty": 0,
|
||||
"max_tokens": 800,
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
}
|
||||
227
plugins/wasm-cpp/extensions/model_router/plugin.cc
Normal file
227
plugins/wasm-cpp/extensions/model_router/plugin.cc
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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("modelKey"); it != configuration.end()) {
|
||||
if (it->is_string()) {
|
||||
rule.model_key_ = it->get<std::string>();
|
||||
} else {
|
||||
LOG_ERROR("Invalid type for modelKey. Expected string.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto it = configuration.find("addProviderHeader");
|
||||
it != configuration.end()) {
|
||||
if (it->is_string()) {
|
||||
rule.add_provider_header_ = it->get<std::string>();
|
||||
} else {
|
||||
LOG_ERROR("Invalid type for addProviderHeader. Expected string.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto it = configuration.find("modelToHeader");
|
||||
it != configuration.end()) {
|
||||
if (it->is_string()) {
|
||||
rule.model_to_header_ = it->get<std::string>();
|
||||
} else {
|
||||
LOG_ERROR("Invalid type for modelToHeader. Expected string.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "enableOnPathSuffix", [&](const json& item) -> bool {
|
||||
if (item.is_string()) {
|
||||
rule.enable_on_path_suffix_.emplace_back(item.get<std::string>());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
LOG_ERROR("Invalid type for item in enableOnPathSuffix. Expected string.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_ERROR("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_ERROR(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseRuleConfig(result.value())) {
|
||||
LOG_ERROR(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginRootContext::onHeader(
|
||||
const ModelRouterConfigRule& rule) {
|
||||
if (!Wasm::Common::Http::hasRequestBody()) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto path = getRequestHeader(Wasm::Common::Http::Header::Path)->toString();
|
||||
auto params_pos = path.find('?');
|
||||
size_t uri_end;
|
||||
if (params_pos == std::string::npos) {
|
||||
uri_end = path.size();
|
||||
} else {
|
||||
uri_end = params_pos;
|
||||
}
|
||||
bool enable = false;
|
||||
for (const auto& enable_suffix : rule.enable_on_path_suffix_) {
|
||||
if (absl::EndsWith({path.c_str(), uri_end}, enable_suffix)) {
|
||||
enable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!enable) {
|
||||
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_provider_header = rule.add_provider_header_;
|
||||
const auto& model_to_header = rule.model_to_header_;
|
||||
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];
|
||||
if (!model_to_header.empty()) {
|
||||
replaceRequestHeader(model_to_header, model_value);
|
||||
}
|
||||
if (!add_provider_header.empty()) {
|
||||
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_provider_header, 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 to provider 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
|
||||
86
plugins/wasm-cpp/extensions/model_router/plugin.h
Normal file
86
plugins/wasm-cpp/extensions/model_router/plugin.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 {
|
||||
std::string model_key_ = "model";
|
||||
std::string add_provider_header_;
|
||||
std::string model_to_header_;
|
||||
std::vector<std::string> enable_on_path_suffix_ = {"/v1/chat/completions"};
|
||||
};
|
||||
|
||||
// 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
|
||||
250
plugins/wasm-cpp/extensions/model_router/plugin_test.cc
Normal file
250
plugins/wasm-cpp/extensions/model_router/plugin_test.cc
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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";
|
||||
} else if (header == ":path") {
|
||||
*result = path_;
|
||||
}
|
||||
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_;
|
||||
std::string path_;
|
||||
BufferBase body_;
|
||||
BufferBase config_;
|
||||
};
|
||||
|
||||
TEST_F(ModelRouterTest, RewriteModelAndHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"addProviderHeader": "x-higress-llm-provider"
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/v1/chat/completions";
|
||||
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, ModelToHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"modelToHeader": "x-higress-llm-model"
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/v1/chat/completions";
|
||||
std::string request_json = R"({"model": "qwen-long"})";
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.Times(0);
|
||||
|
||||
EXPECT_CALL(
|
||||
*mock_context_,
|
||||
replaceHeaderMapValue(testing::_, std::string_view("x-higress-llm-model"),
|
||||
std::string_view("qwen-long")));
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, IgnorePath) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"addProviderHeader": "x-higress-llm-provider"
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/v1/chat/xxxx";
|
||||
std::string request_json = R"({"model": "qwen/qwen-long"})";
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.Times(0);
|
||||
|
||||
EXPECT_CALL(*mock_context_,
|
||||
replaceHeaderMapValue(testing::_,
|
||||
std::string_view("x-higress-llm-provider"),
|
||||
std::string_view("qwen")))
|
||||
.Times(0);
|
||||
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["route-a"],
|
||||
"addProviderHeader": "x-higress-llm-provider"
|
||||
}
|
||||
]})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/api/v1/chat/completions";
|
||||
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
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "request_block.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -27,7 +27,6 @@ wasm_cc_binary(
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# 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("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
proxy_wasm_cc_binary(
|
||||
name = "sni_misdirect.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
@@ -25,7 +25,6 @@ wasm_cc_binary(
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/strings",
|
||||
"//common:http_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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,支持多轮对话,支持流式与非流式模式,支持将结果格式化为自定义的 json。
|
||||
agent流程图如下:
|
||||

|
||||

|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`20`
|
||||
插件执行优先级:`200`
|
||||
|
||||
## 配置字段
|
||||
|
||||
@@ -22,6 +21,7 @@ agent流程图如下:
|
||||
| `llm` | object | 必填 | - | 配置 AI 服务提供商的信息 |
|
||||
| `apis` | object | 必填 | - | 配置外部 API 服务提供商的信息 |
|
||||
| `promptTemplate` | object | 非必填 | - | 配置 Agent ReAct 模板的信息 |
|
||||
| `jsonResp` | object | 非必填 | - | 配置 json 格式化的相关信息 |
|
||||
|
||||
`llm`的配置字段说明如下:
|
||||
|
||||
@@ -46,18 +46,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,13 +76,17 @@ 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 部分 |
|
||||
|
||||
## 用法示例
|
||||
`jsonResp`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|-----------|---------|--------|-----------------------------------|
|
||||
| `enable` | bool | 非必填 | false | 是否开启 json 格式化。 |
|
||||
| `jsonSchema` | string | 非必填 | - | 自定义 json schema |
|
||||
|
||||
## 用法示例-不开启 json 格式化
|
||||
|
||||
**配置信息**
|
||||
|
||||
@@ -296,7 +301,7 @@ deepl提供了一个工具,用于翻译给定的句子,支持多语言。。
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
curl 'http://<这里换成网关地址>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"我想在济南市鑫盛大厦附近喝咖啡,给我推荐几个"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
@@ -311,7 +316,7 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
curl 'http://<这里换成网关地址>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
@@ -326,7 +331,22 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
curl 'http://<这里换成网关地址>/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://<这里换成网关地址>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?用华氏度表示,用日语回答"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
@@ -341,7 +361,7 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
curl 'http://<这里换成网关地址>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"帮我用德语翻译以下句子:九头蛇万岁!"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
@@ -352,3 +372,71 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
```json
|
||||
{"id":"65dcf12c-61ff-9e68-bffa-44fc9e6070d5","choices":[{"index":0,"message":{"role":"assistant","content":" “九头蛇万岁!”的德语翻译为“Hoch lebe Hydra!”。"},"finish_reason":"stop"}],"created":1724043865,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":908,"completion_tokens":52,"total_tokens":960}}
|
||||
```
|
||||
|
||||
## 用法示例-开启 json 格式化
|
||||
|
||||
**配置信息**
|
||||
在上述配置的基础上增加 jsonResp 配置
|
||||
```yaml
|
||||
jsonResp:
|
||||
enable: true
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关地址>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"北京市现在的天气情况如何?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content": "{\"city\": \"北京市\", \"weather_condition\": \"多云\", \"temperature\": \"19℃\", \"data_update_time\": \"2024年10月9日16时37分53秒\"}"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
如果不自定义 json schema,大模型会自动生成一个 json 格式
|
||||
|
||||
**配置信息**
|
||||
增加自定义 json schema 配置
|
||||
```yaml
|
||||
jsonResp:
|
||||
enable: true
|
||||
jsonSchema: |
|
||||
title: WeatherSchema
|
||||
type: object
|
||||
properties:
|
||||
location:
|
||||
type: string
|
||||
description: 城市名称.
|
||||
weather:
|
||||
type: string
|
||||
description: 天气情况.
|
||||
temperature:
|
||||
type: string
|
||||
description: 温度.
|
||||
update_time:
|
||||
type: string
|
||||
description: 数据更新时间.
|
||||
required:
|
||||
- location
|
||||
- weather
|
||||
- temperature
|
||||
additionalProperties: false
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关地址>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"北京市现在的天气情况如何?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content": "{\"location\": \"北京市\", \"weather\": \"多云\", \"temperature\": \"19℃\", \"update_time\": \"2024年10月9日16时37分53秒\"}"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
@@ -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, support for formatting results as custom json.
|
||||
The agent flow chart is as follows:
|
||||

|
||||
|
||||
## Runtime Properties
|
||||
Plugin execution phase: `Default Phase`
|
||||
Plugin execution priority: `20`
|
||||
Plugin execution priority: `200`
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
@@ -20,6 +20,7 @@ Plugin execution priority: `20`
|
||||
| `llm` | object | Required | - | Configuration information for AI service provider |
|
||||
| `apis` | object | Required | - | Configuration information for external API service provider |
|
||||
| `promptTemplate` | object | Optional | - | Configuration information for Agent ReAct template |
|
||||
| `jsonResp` | object | Optional | - | Configuring json formatting information |
|
||||
|
||||
The configuration fields for `llm` are as follows:
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
@@ -41,17 +42,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,13 +69,16 @@ 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
|
||||
The configuration fields for `jsonResp` are as follows:
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
|--------------------|-----------|-------------|---------------|------------------------------------|
|
||||
| `enable` | bool | Optional | - | Whether to enable json formatting. |
|
||||
| `jsonSchema` | string | Optional | - | Custom json schema |
|
||||
|
||||
## Usage Example-disable json formatting
|
||||
**Configuration Information**
|
||||
```yaml
|
||||
llm:
|
||||
@@ -308,6 +313,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**
|
||||
@@ -326,3 +342,68 @@ curl 'http://<replace with gateway public IP>/api/openai/v1/chat/completions' \
|
||||
{"id":"65dcf12c-61ff-9e68-bffa-44fc9e6070d5","choices":[{"index":0,"message":{"role":"assistant","content":" The German translation of \"Hail Hydra!\" is \"Hoch lebe Hydra!\"."},"finish_reason":"stop"}],"created":1724043865,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":908,"completion_tokens":52,"total_tokens":960}}
|
||||
```
|
||||
|
||||
## Usage Example-enable json formatting
|
||||
**Configuration Information**
|
||||
Add jsonResp configuration to the above configuration
|
||||
```yaml
|
||||
jsonResp:
|
||||
enable: true
|
||||
```
|
||||
|
||||
**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 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": "{\"city\": \"BeiJing\", \"weather_condition\": \"cloudy\", \"temperature\": \"19℃\", \"data_update_time\": \"Oct 9, 2024, at 16:37\"}"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
If you don't customise the json schema, the big model will automatically generate a json format
|
||||
|
||||
**Configuration Information**
|
||||
Add custom json schema configuration
|
||||
```yaml
|
||||
jsonResp:
|
||||
enable: true
|
||||
jsonSchema:
|
||||
title: WeatherSchema
|
||||
type: object
|
||||
properties:
|
||||
location:
|
||||
type: string
|
||||
description: city name.
|
||||
weather:
|
||||
type: string
|
||||
description: weather conditions.
|
||||
temperature:
|
||||
type: string
|
||||
description: temperature.
|
||||
update_time:
|
||||
type: string
|
||||
description: the update time of data.
|
||||
required:
|
||||
- location
|
||||
- weather
|
||||
- temperature
|
||||
additionalProperties: false
|
||||
```
|
||||
|
||||
**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 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": "{\"location\": \"Beijing\", \"weather\": \"cloudy\", \"temperature\": \"19℃\", \"update_time\": \"Oct 9, 2024, at 16:37\"}"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
@@ -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"`
|
||||
@@ -210,6 +211,15 @@ type LLMInfo struct {
|
||||
MaxTokens int64 `yaml:"maxToken" json:"maxTokens"`
|
||||
}
|
||||
|
||||
type JsonResp struct {
|
||||
// @Title zh-CN Enable
|
||||
// @Description zh-CN 是否要启用json格式化输出
|
||||
Enable bool `yaml:"enable" json:"enable"`
|
||||
// @Title zh-CN Json Schema
|
||||
// @Description zh-CN 用以验证响应json的Json Schema, 为空则只验证返回的响应是否为合法json
|
||||
JsonSchema map[string]interface{} `required:"false" json:"jsonSchema" yaml:"jsonSchema"`
|
||||
}
|
||||
|
||||
type PluginConfig struct {
|
||||
// @Title zh-CN 返回 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
@@ -222,8 +232,9 @@ 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"`
|
||||
JsonResp JsonResp `yaml:"jsonResp" json:"jsonResp"`
|
||||
}
|
||||
|
||||
func initResponsePromptTpl(gjson gjson.Result, c *PluginConfig) {
|
||||
@@ -260,11 +271,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 +302,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 +332,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 +352,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 +390,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 +409,18 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
func initJsonResp(gjson gjson.Result, c *PluginConfig) {
|
||||
c.JsonResp.Enable = false
|
||||
if c.JsonResp.Enable = gjson.Get("jsonResp.enable").Bool(); c.JsonResp.Enable {
|
||||
c.JsonResp.JsonSchema = nil
|
||||
if jsonSchemaValue := gjson.Get("jsonResp.jsonSchema"); jsonSchemaValue.Exists() {
|
||||
if schemaValue, ok := jsonSchemaValue.Value().(map[string]interface{}); ok {
|
||||
c.JsonResp.JsonSchema = schemaValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
|
||||
// 用于统计函数的递归调用次数
|
||||
const ToolCallsCount = "ToolCallsCount"
|
||||
const StreamContextKey = "Stream"
|
||||
|
||||
// react的正则规则
|
||||
const ActionPattern = `Action:\s*(.*?)[.\n]`
|
||||
@@ -46,6 +49,8 @@ func parseConfig(gjson gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
|
||||
initLLMClient(gjson, c)
|
||||
|
||||
initJsonResp(gjson, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +58,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,19 +67,23 @@ 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("替换失败")
|
||||
log.Debugf("failed replace err: %s", err.Error())
|
||||
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, "替换失败"+err.Error())), -1)
|
||||
}
|
||||
log.Debug("[onHttpRequestBody] request替换成功")
|
||||
log.Debug("[onHttpRequestBody] replace request success")
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
@@ -87,18 +96,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 +128,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 +139,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 +167,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
|
||||
}
|
||||
@@ -166,11 +179,103 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wra
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Debugf("statusCode: %d\n", statusCode)
|
||||
func extractJson(bodyStr string) (string, error) {
|
||||
// simply extract json from response body string
|
||||
startIndex := strings.Index(bodyStr, "{")
|
||||
endIndex := strings.LastIndex(bodyStr, "}") + 1
|
||||
|
||||
// if not found
|
||||
if startIndex == -1 || startIndex >= endIndex {
|
||||
return "", errors.New("cannot find json in the response body")
|
||||
}
|
||||
log.Info("========函数返回结果========")
|
||||
|
||||
jsonStr := bodyStr[startIndex:endIndex]
|
||||
|
||||
// attempt to parse the JSON
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal([]byte(jsonStr), &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsonStr, nil
|
||||
}
|
||||
|
||||
func jsonFormat(llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonSchema map[string]interface{}, assistantMessage Message, actionInput string, headers [][2]string, streamMode bool, rawResponse Response, log wrapper.Log) string {
|
||||
prompt := fmt.Sprintf(prompttpl.Json_Resp_Template, jsonSchema, actionInput)
|
||||
|
||||
messages := []dashscope.Message{{Role: "user", Content: prompt}}
|
||||
|
||||
completion := dashscope.Completion{
|
||||
Model: llmInfo.Model,
|
||||
Messages: messages,
|
||||
}
|
||||
|
||||
completionSerialized, _ := json.Marshal(completion)
|
||||
var content string
|
||||
err := llmClient.Post(
|
||||
llmInfo.Path,
|
||||
headers,
|
||||
completionSerialized,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
//得到gpt的返回结果
|
||||
var responseCompletion dashscope.CompletionResponse
|
||||
_ = json.Unmarshal(responseBody, &responseCompletion)
|
||||
log.Infof("[jsonFormat] content: %s", responseCompletion.Choices[0].Message.Content)
|
||||
content = responseCompletion.Choices[0].Message.Content
|
||||
jsonStr, err := extractJson(content)
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] extractJson err: %s", err.Error())
|
||||
jsonStr = content
|
||||
}
|
||||
|
||||
if streamMode {
|
||||
stream(jsonStr, rawResponse, log)
|
||||
} else {
|
||||
noneStream(assistantMessage, jsonStr, rawResponse, log)
|
||||
}
|
||||
}, uint32(llmInfo.MaxExecutionTime))
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] completion err: %s", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func noneStream(assistantMessage Message, actionInput string, rawResponse Response, log wrapper.Log) {
|
||||
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)
|
||||
|
||||
log.Debug("[onHttpResponseBody] replace response success")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}
|
||||
}
|
||||
|
||||
func stream(actionInput string, rawResponse Response, log wrapper.Log) {
|
||||
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] replace response success")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}
|
||||
|
||||
func toolsCallResult(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonResp JsonResp, aPIsParam []APIsParam, aPIClient []wrapper.HttpClient, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Debugf("statusCode: %d", statusCode)
|
||||
}
|
||||
log.Info("========function result========")
|
||||
log.Infof(string(responseBody))
|
||||
|
||||
observation := "Observation: " + string(responseBody)
|
||||
@@ -178,175 +283,234 @@ func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content strin
|
||||
dashscope.MessageStore.AddForUser(observation)
|
||||
|
||||
completion := dashscope.Completion{
|
||||
Model: config.LLMInfo.Model,
|
||||
Model: llmInfo.Model,
|
||||
Messages: dashscope.MessageStore,
|
||||
MaxTokens: config.LLMInfo.MaxTokens,
|
||||
MaxTokens: llmInfo.MaxTokens,
|
||||
}
|
||||
|
||||
headers := [][2]string{{"Content-Type", "application/json"}, {"Authorization", "Bearer " + config.LLMInfo.APIKey}}
|
||||
headers := [][2]string{{"Content-Type", "application/json"}, {"Authorization", "Bearer " + llmInfo.APIKey}}
|
||||
completionSerialized, _ := json.Marshal(completion)
|
||||
err := config.LLMClient.Post(
|
||||
config.LLMInfo.Path,
|
||||
err := llmClient.Post(
|
||||
llmInfo.Path,
|
||||
headers,
|
||||
completionSerialized,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
//得到gpt的返回结果
|
||||
var responseCompletion dashscope.CompletionResponse
|
||||
_ = json.Unmarshal(responseBody, &responseCompletion)
|
||||
log.Infof("[toolsCall] content: %s\n", responseCompletion.Choices[0].Message.Content)
|
||||
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, llmClient, llmInfo, jsonResp, aPIsParam, aPIClient, responseCompletion.Choices[0].Message.Content, rawResponse, log)
|
||||
if retType == types.ActionContinue {
|
||||
//得到了Final Answer
|
||||
var assistantMessage Message
|
||||
assistantMessage.Role = "assistant"
|
||||
startIndex := strings.Index(responseCompletion.Choices[0].Message.Content, "Final Answer:")
|
||||
if startIndex != -1 {
|
||||
startIndex += len("Final Answer:") // 移动到"Final Answer:"之后的位置
|
||||
extractedText := responseCompletion.Choices[0].Message.Content[startIndex:]
|
||||
assistantMessage.Content = extractedText
|
||||
}
|
||||
|
||||
rawResponse.Choices[0].Message = assistantMessage
|
||||
|
||||
newbody, err := json.Marshal(rawResponse)
|
||||
if err != nil {
|
||||
proxywasm.ResumeHttpResponse()
|
||||
return
|
||||
var streamMode bool
|
||||
if ctx.GetContext(StreamContextKey) == nil {
|
||||
streamMode = false
|
||||
if jsonResp.Enable {
|
||||
jsonFormat(llmClient, llmInfo, jsonResp.JsonSchema, assistantMessage, actionInput, headers, streamMode, rawResponse, log)
|
||||
} else {
|
||||
noneStream(assistantMessage, actionInput, rawResponse, log)
|
||||
}
|
||||
} else {
|
||||
log.Infof("[onHttpResponseBody] newResponseBody: ", string(newbody))
|
||||
proxywasm.ReplaceHttpResponseBody(newbody)
|
||||
|
||||
log.Debug("[onHttpResponseBody] response替换成功")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
streamMode = true
|
||||
if jsonResp.Enable {
|
||||
jsonFormat(llmClient, llmInfo, jsonResp.JsonSchema, assistantMessage, actionInput, headers, streamMode, rawResponse, log)
|
||||
} else {
|
||||
stream(actionInput, rawResponse, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}, uint32(config.LLMInfo.MaxExecutionTime))
|
||||
}, uint32(llmInfo.MaxExecutionTime))
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] completion err: %s", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) types.Action {
|
||||
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, llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonResp JsonResp, aPIsParam []APIsParam, aPIClient []wrapper.HttpClient, 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, llmInfo.MaxIterations)
|
||||
//函数递归调用次数,达到了预设的循环次数,强制结束
|
||||
if int64(count) > config.LLMInfo.MaxIterations {
|
||||
if int64(count) > 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 urlStr string
|
||||
var headers [][2]string
|
||||
var apiClient wrapper.HttpClient
|
||||
var method string
|
||||
var reqBody []byte
|
||||
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 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), &data); err != nil {
|
||||
log.Debugf("Error: %s", err.Error())
|
||||
return types.ActionContinue, ""
|
||||
}
|
||||
|
||||
//将大模型需要的参数反序列化
|
||||
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 = tools_param.Method
|
||||
|
||||
method = tool_param.Method
|
||||
// 组装 URL 和请求体
|
||||
urlStr = apisParam.URL + tools_param.Path
|
||||
|
||||
//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模板以查找路径参数
|
||||
urlParts := strings.Split(urlStr, "/")
|
||||
for i, part := range urlParts {
|
||||
if strings.Contains(part, "{") && strings.Contains(part, "}") {
|
||||
for _, param := range tools_param.ParamName {
|
||||
paramNameInPath := part[1 : len(part)-1]
|
||||
if paramNameInPath == param {
|
||||
if value, ok := data[param]; ok {
|
||||
// 删除已经使用过的
|
||||
delete(data, param)
|
||||
// 替换模板中的占位符
|
||||
urlParts[i] = url.QueryEscape(value.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//url组装
|
||||
url = apiParam.URL + tool_param.Path + key + args
|
||||
} else if method == "POST" {
|
||||
reqBody = nil
|
||||
//json参数组装
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Debugf("Error: %s\n", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
reqBody = jsonData
|
||||
|
||||
//url组装
|
||||
url = apiParam.URL + tool_param.Path + key
|
||||
}
|
||||
|
||||
log.Infof("url: %s\n", url)
|
||||
|
||||
apiClient = config.APIClient[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if apiClient != nil {
|
||||
err := apiClient.Call(
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
reqBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
toolsCallResult(ctx, config, content, rawResponse, log, statusCode, responseBody)
|
||||
}, 50000)
|
||||
if err != nil {
|
||||
log.Debugf("tool calls error: %s\n", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
// 重新组合URL
|
||||
urlStr = strings.Join(urlParts, "/")
|
||||
|
||||
queryParams := make([][2]string, 0)
|
||||
if method == "GET" {
|
||||
for _, param := range tools_param.ParamName {
|
||||
if value, ok := data[param]; ok {
|
||||
queryParams = append(queryParams, [2]string{param, fmt.Sprintf("%v", value)})
|
||||
}
|
||||
}
|
||||
} 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, ""
|
||||
}
|
||||
}
|
||||
|
||||
// 组装 headers 和 key
|
||||
headers = [][2]string{{"Content-Type", "application/json"}}
|
||||
if apisParam.APIKey.Name != "" {
|
||||
if apisParam.APIKey.In == "query" {
|
||||
queryParams = append(queryParams, [2]string{apisParam.APIKey.Name, apisParam.APIKey.Value})
|
||||
} else if apisParam.APIKey.In == "header" {
|
||||
headers = append(headers, [2]string{"Authorization", apisParam.APIKey.Name + " " + apisParam.APIKey.Value})
|
||||
}
|
||||
}
|
||||
|
||||
if len(queryParams) > 0 {
|
||||
// 将 key 拼接到 url 后面
|
||||
urlStr += "?"
|
||||
for i, param := range queryParams {
|
||||
if i != 0 {
|
||||
urlStr += "&"
|
||||
}
|
||||
urlStr += url.QueryEscape(param[0]) + "=" + url.QueryEscape(param[1])
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("url: %s", urlStr)
|
||||
|
||||
apiClient = aPIClient[i]
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionPause
|
||||
|
||||
if apiClient != nil {
|
||||
err := apiClient.Call(
|
||||
method,
|
||||
urlStr,
|
||||
headers,
|
||||
reqBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
toolsCallResult(ctx, llmClient, llmInfo, jsonResp, aPIsParam, aPIClient, 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 +525,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.LLMClient, config.LLMInfo, config.JsonResp, config.APIsParam, config.APIClient, rawResponse.Choices[0].Message.Content, rawResponse, log)
|
||||
return retType
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -13,81 +13,161 @@ 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
|
||||
`
|
||||
const Json_Resp_Template = `
|
||||
Given the Json Schema: %s, please help me convert the following content to a pure json: %s
|
||||
Do not respond other content except the pure json!!!!
|
||||
`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# File generated by hgctl. Modify as required.
|
||||
|
||||
docker-compose-test/
|
||||
*
|
||||
|
||||
!/.gitignore
|
||||
|
||||
@@ -1,46 +1,176 @@
|
||||
## 简介
|
||||
---
|
||||
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 请求/响应提取参数配置等。
|
||||
|
||||
| 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 替换的部分 |
|
||||
## 配置说明
|
||||
|
||||
本插件同时支持基于向量数据库的语义化缓存和基于字符串匹配的缓存方法,如果同时配置了向量数据库和缓存数据库,优先使用向量数据库。
|
||||
|
||||
*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 | "" | 向量存储服务 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 替换的部分 |
|
||||
|
||||
# 向量数据库提供商特有配置
|
||||
## Chroma
|
||||
Chroma 所对应的 `vector.type` 为 `chroma`。它并无特有的配置字段。需要提前创建 Collection,并填写 Collection ID 至配置项 `vector.collectionID`,一个 Collection ID 的示例为 `52bbb8b3-724c-477b-a4ce-d5b578214612`。
|
||||
|
||||
## DashVector
|
||||
DashVector 所对应的 `vector.type` 为 `dashvector`。它并无特有的配置字段。需要提前创建 Collection,并填写 `Collection 名称` 至配置项 `vector.collectionID`。
|
||||
|
||||
## ElasticSearch
|
||||
ElasticSearch 所对应的 `vector.type` 为 `elasticsearch`。需要提前创建 Index 并填写 Index Name 至配置项 `vector.collectionID` 。
|
||||
|
||||
当前依赖于 [KNN](https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html) 方法,请保证 ES 版本支持 `KNN`,当前已在 `8.16` 版本测试。
|
||||
|
||||
它特有的配置字段如下:
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|----------|----------|--------|-------------------------------------------------------------------------------|
|
||||
| `vector.esUsername` | string | 非必填 | - | ElasticSearch 用户名 |
|
||||
| `vector.esPassword` | string | 非必填 | - | ElasticSearch 密码 |
|
||||
|
||||
|
||||
`vector.esUsername` 和 `vector.esPassword` 用于 Basic 认证。同时也支持 Api Key 认证,当填写了 `vector.apiKey` 时,则启用 Api Key 认证,如果使用 SaaS 版本需要填写 `encoded` 的值。
|
||||
|
||||
## Milvus
|
||||
Milvus 所对应的 `vector.type` 为 `milvus`。它并无特有的配置字段。需要提前创建 Collection,并填写 Collection Name 至配置项 `vector.collectionID`。
|
||||
|
||||
## Pinecone
|
||||
Pinecone 所对应的 `vector.type` 为 `pinecone`。它并无特有的配置字段。需要提前创建 Index,并填写 Index 访问域名至 `vector.serviceHost`。
|
||||
|
||||
Pinecone 中的 `Namespace` 参数通过插件的 `vector.collectionID` 进行配置,如果不填写 `vector.collectionID`,则默认为 Default Namespace。
|
||||
|
||||
## Qdrant
|
||||
Qdrant 所对应的 `vector.type` 为 `qdrant`。它并无特有的配置字段。需要提前创建 Collection,并填写 Collection Name 至配置项 `vector.collectionID`。
|
||||
|
||||
## Weaviate
|
||||
Weaviate 所对应的 `vector.type` 为 `weaviate`。它并无特有的配置字段。
|
||||
需要提前创建 Collection,并填写 Collection Name 至配置项 `vector.collectionID`。
|
||||
|
||||
需要注意的是 Weaviate 会设置首字母自动大写,在填写配置 `collectionID` 的时候需要将首字母设置为大写。
|
||||
|
||||
如果使用 SaaS 需要填写 `vector.serviceHost` 参数。
|
||||
|
||||
## 配置示例
|
||||
### 基础配置
|
||||
```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 +180,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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user