mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d98f8b8b21 | ||
|
|
bd19a5049b | ||
|
|
1070541f1d | ||
|
|
32b5c89c17 | ||
|
|
bd1101d711 | ||
|
|
27680223b9 | ||
|
|
93ea5e7355 | ||
|
|
ff9a29c5d9 | ||
|
|
6a1557f6ac | ||
|
|
e6e4193679 | ||
|
|
978d0afb63 | ||
|
|
39dd4538c9 | ||
|
|
f826d79109 | ||
|
|
7348c265b5 | ||
|
|
ea0bf7c1b7 | ||
|
|
ba1bf353b8 | ||
|
|
b56097e647 | ||
|
|
5b97b849b5 | ||
|
|
331fe57c70 | ||
|
|
4d32cc9468 | ||
|
|
34b5a6feea | ||
|
|
8736edaf61 | ||
|
|
30d5b4d32e | ||
|
|
c0133378a7 | ||
|
|
8346b4a4a2 | ||
|
|
ce271849de | ||
|
|
bdc3ecab71 | ||
|
|
9214dca078 | ||
|
|
c3eb8d0447 | ||
|
|
081ab6ee8d | ||
|
|
9a45f07972 | ||
|
|
da2ae4c7ee | ||
|
|
ff068258a1 | ||
|
|
0996ad21b1 | ||
|
|
45eb76d4cc | ||
|
|
36bcb595d6 | ||
|
|
783a8db512 | ||
|
|
44566f5259 | ||
|
|
73ba9238bd | ||
|
|
41a1455874 | ||
|
|
9d68ccbf35 | ||
|
|
db7dbb24a2 | ||
|
|
9a0cf9b762 | ||
|
|
bb786c9618 | ||
|
|
ef49d2f5f6 | ||
|
|
864bf5af39 | ||
|
|
527e922d50 | ||
|
|
1fe5eb6e13 | ||
|
|
87185baff2 | ||
|
|
76ada0b844 | ||
|
|
f4d3fec228 | ||
|
|
e94ac43dd1 | ||
|
|
dd29267fd7 | ||
|
|
01a9161153 | ||
|
|
ceb8b557dc | ||
|
|
753022e093 | ||
|
|
04cbbfc7e8 | ||
|
|
db66df39c4 | ||
|
|
dad6278a6d | ||
|
|
272d693df3 | ||
|
|
69bc800198 | ||
|
|
1daaa4b880 | ||
|
|
6e31a7b67c | ||
|
|
91f070906a | ||
|
|
e3aeddcc24 | ||
|
|
926913f0e7 | ||
|
|
c471bb2003 | ||
|
|
0b9256617e | ||
|
|
2670ecbf8e | ||
|
|
7040e4bd34 | ||
|
|
de8a4d0b03 | ||
|
|
b33a3a4d2e | ||
|
|
087cb48fc5 | ||
|
|
95f32002d2 | ||
|
|
fb8dd819e9 | ||
|
|
86934b3203 | ||
|
|
38068ee43d | ||
|
|
d81573e0d2 | ||
|
|
312b80f91d | ||
|
|
e42e6eeee6 | ||
|
|
9f5067d22f | ||
|
|
6af9587372 | ||
|
|
5812c1e734 | ||
|
|
bafbe7972d | ||
|
|
f3fbf7d6c8 | ||
|
|
1666dfb01c | ||
|
|
d2f09fe8c5 | ||
|
|
69d877c116 | ||
|
|
5bc0058779 | ||
|
|
d4e114b152 | ||
|
|
e674c780c6 | ||
|
|
26cd6837d5 | ||
|
|
5674d91a10 | ||
|
|
c78b4aaba3 | ||
|
|
0e4e8da9c1 | ||
|
|
c9ec8a12bb | ||
|
|
7484bcea62 | ||
|
|
896780b60e | ||
|
|
7b1ae49cd4 | ||
|
|
ee26baf054 | ||
|
|
33fc47cefb | ||
|
|
19946d46ca | ||
|
|
52d0212698 | ||
|
|
a73c33f1da | ||
|
|
69b755a10d | ||
|
|
52464c0e06 | ||
|
|
d7d5d1c571 | ||
|
|
ea948ee818 | ||
|
|
767f51adce | ||
|
|
168cb04c61 | ||
|
|
323aabf72b | ||
|
|
b8d75598ed | ||
|
|
b37649a62f | ||
|
|
76f76a70ab | ||
|
|
647c961f51 | ||
|
|
5a5a72a9f8 | ||
|
|
ffcf5df28a | ||
|
|
ec83623614 | ||
|
|
bf5be07d74 | ||
|
|
f6bb5d7729 | ||
|
|
031ae21caa | ||
|
|
fa3c5ea0fc | ||
|
|
93436db13c | ||
|
|
be2c6f8a4a | ||
|
|
c768973e47 | ||
|
|
8ec65ed377 | ||
|
|
675a8ce4a9 | ||
|
|
06c5ddd80b | ||
|
|
8ccc170500 | ||
|
|
ff308d5292 | ||
|
|
af8502b0b0 | ||
|
|
c683936b1c | ||
|
|
8b3f1aab1a | ||
|
|
b5eadcdbee | ||
|
|
8ca8fd27ab | ||
|
|
ab014cf912 | ||
|
|
3f67b05fab | ||
|
|
cd271c1f87 | ||
|
|
755de5ae67 | ||
|
|
40402e7dbd | ||
|
|
0a2fb35ae2 | ||
|
|
b16954d8c1 | ||
|
|
29370b18d7 | ||
|
|
c9733d405c | ||
|
|
ec6004dd27 | ||
|
|
ea9a6de8c3 | ||
|
|
5e40a700ae |
@@ -3,22 +3,22 @@ name: Build and Push Wasm Plugin Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
|
||||
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
plugin_type:
|
||||
description: 'Type of the plugin'
|
||||
description: "Type of the plugin"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- go
|
||||
- rust
|
||||
plugin_name:
|
||||
description: 'Name of the plugin'
|
||||
description: "Name of the plugin"
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: 'Version of the plugin (optional, without leading v)'
|
||||
description: "Version of the plugin (optional, without leading v)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -31,8 +31,7 @@ jobs:
|
||||
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
|
||||
GO_VERSION: 1.24.0
|
||||
ORAS_VERSION: 1.0.0
|
||||
steps:
|
||||
- name: Set plugin_type, plugin_name and version from inputs or ref_name
|
||||
@@ -53,7 +52,7 @@ jobs:
|
||||
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 }}"
|
||||
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-oras${{ env.ORAS_VERSION }}"
|
||||
fi
|
||||
echo "PLUGIN_TYPE=$plugin_type" >> $GITHUB_ENV
|
||||
echo "PLUGIN_NAME=$plugin_name" >> $GITHUB_ENV
|
||||
@@ -62,9 +61,9 @@ jobs:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
|
||||
- name: File Check
|
||||
run: |
|
||||
run: |
|
||||
workspace=${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
|
||||
push_command="./plugin.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
|
||||
@@ -79,7 +78,7 @@ jobs:
|
||||
echo "README.md exists"
|
||||
push_command="./README.md:application/vnd.module.wasm.doc.v1+markdown $push_command "
|
||||
fi
|
||||
|
||||
|
||||
# 查找README_{lang}.md
|
||||
for file in ${workspace}/README_*.md; do
|
||||
if [ -f "$file" ]; then
|
||||
@@ -91,9 +90,9 @@ jobs:
|
||||
done
|
||||
|
||||
echo "PUSH_COMMAND=\"$push_command\"" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Run a wasm-builder
|
||||
env:
|
||||
env:
|
||||
PLUGIN_NAME: ${{ env.PLUGIN_NAME }}
|
||||
BUILDER_IMAGE: ${{ env.BUILDER_IMAGE }}
|
||||
run: |
|
||||
@@ -104,7 +103,7 @@ jobs:
|
||||
push_command=${{ env.PUSH_COMMAND }}
|
||||
push_command=${push_command#\"}
|
||||
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号,确保oras push正常解析
|
||||
|
||||
|
||||
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
|
||||
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:latest"
|
||||
echo "TargetImage=${target_image}"
|
||||
@@ -123,7 +122,7 @@ jobs:
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
go mod tidy
|
||||
tinygo build -o ./plugin.wasm -scheduler=none -target=wasi -gc=custom -tags=\"custommalloc nottinygc_finalizer ${EXTRA_TAGS}\" .
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm main.go
|
||||
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}
|
||||
|
||||
32
.github/workflows/build-and-test-plugin.yaml
vendored
32
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -2,20 +2,20 @@ name: "Build and Test Plugins"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
- "plugins/**"
|
||||
- "test/**"
|
||||
- "helm/**"
|
||||
- "Makefile.core.mk"
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
branches: ["*"]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
workflow_dispatch: ~
|
||||
- "plugins/**"
|
||||
- "test/**"
|
||||
- "helm/**"
|
||||
- "Makefile.core.mk"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
go-version: 1.24
|
||||
# There are too many lint errors in current code bases
|
||||
# uncomment when we decide what lint should be addressed or ignored.
|
||||
# - run: make lint
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO(Xunzhuo): Enable C WASM Filters in CI
|
||||
wasmPluginType: [ GO, RUST ]
|
||||
wasmPluginType: [GO, RUST]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -46,12 +46,12 @@ jobs:
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
go-version: 1.24
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -80,6 +80,6 @@ jobs:
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ higress-wasmplugin-test ]
|
||||
needs: [higress-wasmplugin-test]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
echo "Version=$VERSION"
|
||||
# Step 3
|
||||
- name: Upload to OSS
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
uses: go-choppy/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp -r -u ./artifact/ oss://higress-website-cn-hongkong/standalone/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
|
||||
4
.github/workflows/deploy-to-oss.yaml
vendored
4
.github/workflows/deploy-to-oss.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
# Step 2
|
||||
- name: Download Helm Charts Index
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
uses: go-choppy/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
sed -i 's/higress\.io/higress\.cn/g' ./artifact/cn-index.yaml
|
||||
# Step 5
|
||||
- name: Upload to OSS
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
uses: go-choppy/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp -r -u ./artifact/ oss://higress-website-cn-hongkong/helm-charts/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
|
||||
217
.github/workflows/generate-release-notes.yaml
vendored
Normal file
217
.github/workflows/generate-release-notes.yaml
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
name: Generate Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DASHSCOPE_API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
MODEL_NAME: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
MODEL_SERVER: ${{ secrets.MODEL_SERVER }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.24
|
||||
|
||||
- name: Clone GitHub MCP Server
|
||||
run: |
|
||||
git clone https://github.com/github/github-mcp-server.git
|
||||
cd github-mcp-server
|
||||
go build -o ../github-mcp-serve ./cmd/github-mcp-server
|
||||
cd ..
|
||||
chmod u+x github-mcp-serve
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Clone Higress Report Agent
|
||||
run: |
|
||||
git clone https://github.com/higress-group/higress-report-agent.git
|
||||
mv github-mcp-serve higress-report-agent/
|
||||
|
||||
- name: Clean up old release notes
|
||||
run: |
|
||||
RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
CLEAN_VERSION=${RELEASE_VERSION#v}
|
||||
if [ -d "release-notes/${CLEAN_VERSION}" ]; then
|
||||
echo "Removing old release notes directory: release-notes/${CLEAN_VERSION}"
|
||||
rm -rf release-notes/${CLEAN_VERSION}
|
||||
else
|
||||
echo "No old release notes directory found for version ${CLEAN_VERSION}."
|
||||
fi
|
||||
|
||||
- name: Create Release Report Script
|
||||
run: |
|
||||
cat > generate_release_report.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Script to generate release notes for Higress projects
|
||||
|
||||
echo "Fetching GitHub generated release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}..."
|
||||
curl -L \
|
||||
"https://github.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/tag/${RELEASE_VERSION}" \
|
||||
-o release_page.html
|
||||
|
||||
echo "Extracting PR numbers from ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME} release notes..."
|
||||
PR_NUMS=$(cat release_page.html | grep -o "/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pull/[0-9]*" | grep -o "[0-9]*$" | sort -n | uniq | tr '\n' ',')
|
||||
PR_NUMS=${PR_NUMS%,}
|
||||
if [ -z "${PR_NUMS}" ]; then
|
||||
echo "No PR numbers found in release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME} tag=${RELEASE_VERSION}."
|
||||
rm release_page.html
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Identifying important PRs..."
|
||||
IMPORTANT_PR_NUMS=$(cat release_page.html | grep -o "<strong>.*/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pull/[0-9]*.*</strong>" | grep -o "pull/[0-9]*" | grep -o "[0-9]*" | sort -n | uniq | tr '\n' ',')
|
||||
IMPORTANT_PR_NUMS=${IMPORTANT_PR_NUMS%,}
|
||||
|
||||
rm release_page.html
|
||||
|
||||
echo "Extracted PR numbers for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}: ${PR_NUMS}"
|
||||
echo "Important PR numbers: ${IMPORTANT_PR_NUMS}"
|
||||
|
||||
echo "Generating detailed release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}..."
|
||||
cd higress-report-agent
|
||||
pip install uv
|
||||
uv sync
|
||||
if [ -n "${IMPORTANT_PR_NUMS}" ]; then
|
||||
uv run report_main.py --mode 2 --choice 2 --pr_nums ${PR_NUMS} --important_prs ${IMPORTANT_PR_NUMS}
|
||||
else
|
||||
uv run report_main.py --mode 2 --choice 2 --pr_nums ${PR_NUMS}
|
||||
fi
|
||||
cp report.md ../
|
||||
cp report.EN.md ../
|
||||
cd ..
|
||||
|
||||
# 去除主库版本号前缀v,以主库版本号为路径
|
||||
CLEAN_VERSION=${MAIN_RELEASE_VERSION#v}
|
||||
|
||||
echo "Creating release notes directory for main version ${MAIN_RELEASE_VERSION}..."
|
||||
mkdir -p release-notes/${CLEAN_VERSION}
|
||||
|
||||
echo "# ${REPORT_TITLE}" >>release-notes/${CLEAN_VERSION}/README_ZH.md
|
||||
sed 's/# Release Notes//' report.md >>release-notes/${CLEAN_VERSION}/README_ZH.md
|
||||
echo -e "\n" >>release-notes/${CLEAN_VERSION}/README_ZH.md
|
||||
|
||||
echo "# ${REPORT_TITLE}" >>release-notes/${CLEAN_VERSION}/README.md
|
||||
sed 's/# Release Notes//' report.EN.md >>release-notes/${CLEAN_VERSION}/README.md
|
||||
echo -e "\n" >>release-notes/${CLEAN_VERSION}/README.md
|
||||
|
||||
rm report.md
|
||||
rm report.EN.md
|
||||
echo "${REPORT_TITLE} release notes saved to release-notes/${CLEAN_VERSION}/"
|
||||
|
||||
EOF
|
||||
chmod +x generate_release_report.sh
|
||||
|
||||
- name: Generate Release Notes for Higress
|
||||
env:
|
||||
GITHUB_REPO_OWNER: alibaba
|
||||
GITHUB_REPO_NAME: higress
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPORT_TITLE: Higress
|
||||
run: |
|
||||
export MAIN_RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
export RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
bash generate_release_report.sh
|
||||
|
||||
- name: Generate Release Notes for Higress Console
|
||||
env:
|
||||
GITHUB_REPO_OWNER: higress-group
|
||||
GITHUB_REPO_NAME: higress-console
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPORT_TITLE: Higress Console
|
||||
run: |
|
||||
export MAIN_RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
export RELEASE_VERSION=$(grep "^higress-console:" ${GITHUB_WORKSPACE}/DEP_VERSION | head -n1 | sed 's/higress-console: //')
|
||||
bash generate_release_report.sh
|
||||
|
||||
- name: Create Update Release Notes Script
|
||||
run: |
|
||||
cat > update_release_note.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
CLEAN_VERSION=${RELEASE_VERSION#v}
|
||||
|
||||
RELEASE_INFO=$(curl -s -L \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/tags/${RELEASE_VERSION})
|
||||
RELEASE_ID=$(echo $RELEASE_INFO | jq -r .id)
|
||||
|
||||
RELEASE_BODY=$(echo $RELEASE_INFO | jq -r .body)
|
||||
NEW_CONTRIBUTORS=$(echo "$RELEASE_BODY" | awk '/## New Contributors/{flag=1; next} /\*\*Full Changelog\*\*/{flag=0} flag' | sed 's/\\n/\n/g')
|
||||
FULL_CHANGELOG=$(echo "$RELEASE_BODY" | awk '/\*\*Full Changelog\*\*:/{print $0}' | sed 's/\*\*Full Changelog\*\*: //g' | sed 's/\\n/\n/g')
|
||||
|
||||
RELEASE_NOTES=$(cat release-notes/${CLEAN_VERSION}/README.md | sed 's/# /## /g')
|
||||
|
||||
if [[ -n "$NEW_CONTRIBUTORS" ]]; then
|
||||
RELEASE_NOTES="${RELEASE_NOTES}
|
||||
|
||||
## New Contributors
|
||||
|
||||
${NEW_CONTRIBUTORS}"
|
||||
fi
|
||||
if [[ -n "$FULL_CHANGELOG" ]]; then
|
||||
RELEASE_NOTES="${RELEASE_NOTES}
|
||||
|
||||
**Full Changelog**: ${FULL_CHANGELOG}"
|
||||
fi
|
||||
|
||||
JSON_DATA=$(jq -n \
|
||||
--arg body "$RELEASE_NOTES" \
|
||||
'{body: $body}')
|
||||
|
||||
curl -L \
|
||||
-X PATCH \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/${RELEASE_ID} \
|
||||
-d "$JSON_DATA"
|
||||
|
||||
EOF
|
||||
chmod +x update_release_note.sh
|
||||
|
||||
- name: Update Release Notes
|
||||
env:
|
||||
GITHUB_REPO_OWNER: alibaba
|
||||
GITHUB_REPO_NAME: higress
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
export RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
bash update_release_note.sh
|
||||
|
||||
- name: Clean
|
||||
run: |
|
||||
rm generate_release_report.sh
|
||||
rm update_release_note.sh
|
||||
rm -rf higress-report-agent
|
||||
rm -rf github-mcp-server
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Add release notes"
|
||||
branch: add-release-notes
|
||||
title: "Add release notes"
|
||||
body: |
|
||||
This PR adds release notes.
|
||||
|
||||
- Automatically generated by GitHub Actions
|
||||
labels: release notes, automated
|
||||
base: main
|
||||
91
.github/workflows/helm-docs.yaml
vendored
91
.github/workflows/helm-docs.yaml
vendored
@@ -6,11 +6,13 @@ on:
|
||||
- "*"
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- '!helm/higress/README.zh.md'
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- '!helm/higress/README.zh.md'
|
||||
|
||||
jobs:
|
||||
helm:
|
||||
@@ -31,96 +33,9 @@ jobs:
|
||||
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)
|
||||
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/README.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
|
||||
|
||||
translate-readme:
|
||||
needs: helm
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- name: Compare README.md
|
||||
id: compare_readme
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
BASE_BRANCH=main
|
||||
UPSTREAM_REPO=https://github.com/alibaba/higress.git
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
git clone --depth 1 --branch $BASE_BRANCH $UPSTREAM_REPO $TEMP_DIR
|
||||
|
||||
if diff -q "$TEMP_DIR/README.md" README.md > /dev/null; then
|
||||
echo "README.md has no changes in comparison to base branch. Skipping translation."
|
||||
echo "skip_translation=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "README.md has changed in comparison to base branch. Proceeding with translation."
|
||||
echo "skip_translation=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Translate README.md to Chinese
|
||||
if: env.skip_translation == 'false'
|
||||
env:
|
||||
API_URL: ${{ secrets.HIGRESS_OPENAI_API_URL }}
|
||||
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
FILE_CONTENT=$(cat README.md)
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg model "$API_MODEL" \
|
||||
--arg content "$FILE_CONTENT" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [
|
||||
{"role": "system", "content": "You are a translation assistant that translates English Markdown text to Chinese."},
|
||||
{"role": "user", "content": $content}
|
||||
],
|
||||
temperature: 1.1,
|
||||
stream: false
|
||||
}')
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
echo "Response: $RESPONSE"
|
||||
|
||||
echo "$RESPONSE" | jq -c -r '.choices[] | .message.content' > README.zh.new.md
|
||||
|
||||
if [ -f "README.zh.new.md" ]; then
|
||||
echo "Translation completed and saved to README.zh.new.md."
|
||||
else
|
||||
echo "Translation failed or no content returned!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv README.zh.new.md README.zh.md
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.skip_translation == 'false'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update helm translated README.zh.md"
|
||||
branch: update-helm-readme-zh
|
||||
title: "Update helm translated README.zh.md"
|
||||
body: |
|
||||
This PR updates the translated README.zh.md file.
|
||||
|
||||
- Automatically generated by GitHub Actions
|
||||
labels: translation, automated
|
||||
base: main
|
||||
2
.github/workflows/release-crd.yaml
vendored
2
.github/workflows/release-crd.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
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
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
|
||||
6
.github/workflows/release-hgctl.yaml
vendored
6
.github/workflows/release-hgctl.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
zip -q -r hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip out/windows_arm64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
tar -zcvf hgctl_${{ env.HGCTL_VERSION }}_darwin_arm64.tar.gz out/darwin_arm64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
tar -zcvf hgctl_${{ env.HGCTL_VERSION }}_darwin_amd64.tar.gz out/darwin_amd64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
|
||||
36
.github/workflows/sync-crds.yaml
vendored
Normal file
36
.github/workflows/sync-crds.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: "Sync CRDs to Helm Chart"
|
||||
|
||||
on:
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'api/kubernetes/customresourcedefinitions.gen.yaml'
|
||||
|
||||
jobs:
|
||||
sync-crds:
|
||||
name: Sync CRDs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Copy the CRD YAML File to Helm Folder
|
||||
run: |
|
||||
cp api/kubernetes/customresourcedefinitions.gen.yaml helm/core/crds/customresourcedefinitions.gen.yaml
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update CRD file in the helm folder"
|
||||
branch: sync-crds
|
||||
title: "Update CRD file in the helm folder"
|
||||
body: |
|
||||
This PR updates CRD file in the helm folder.
|
||||
|
||||
- Automatically copied by GitHub Actions
|
||||
labels: crds, automated
|
||||
base: main
|
||||
131
.github/workflows/translate-readme.yaml
vendored
Normal file
131
.github/workflows/translate-readme.yaml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
name: "Helm Docs"
|
||||
|
||||
on:
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/higress/README.md'
|
||||
|
||||
jobs:
|
||||
translate-readme:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- name: Compare README.md
|
||||
id: compare_readme
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
|
||||
BASE_BRANCH=${GITHUB_BASE_REF:-main}
|
||||
git fetch origin $BASE_BRANCH
|
||||
|
||||
if git diff --quiet origin/$BASE_BRANCH -- README.md; then
|
||||
echo "README.md has no local changes compared to $BASE_BRANCH. Skipping translation."
|
||||
echo "skip_translation=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "README.md has local changes compared to $BASE_BRANCH. Proceeding with translation."
|
||||
echo "skip_translation=false" >> $GITHUB_ENV
|
||||
echo "--------- diff ---------"
|
||||
git diff origin/$BASE_BRANCH -- README.md
|
||||
echo "------------------------"
|
||||
fi
|
||||
|
||||
- name: Translate README.md to Chinese
|
||||
if: env.skip_translation == 'false'
|
||||
env:
|
||||
API_URL: ${{ secrets.HIGRESS_OPENAI_API_URL }}
|
||||
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
run: |
|
||||
cat << 'EOF' > translate_readme.py
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
API_URL = os.environ["API_URL"]
|
||||
API_KEY = os.environ["API_KEY"]
|
||||
API_MODEL = os.environ["API_MODEL"]
|
||||
README_PATH = "./helm/higress/README.md"
|
||||
OUTPUT_PATH = "./helm/higress/README.zh.md"
|
||||
|
||||
def stream_translation(api_url, api_key, payload):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
response = requests.post(api_url, headers=headers, json=payload, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as out_file:
|
||||
for line in response.iter_lines(decode_unicode=True):
|
||||
if line.strip() == "" or not line.startswith("data: "):
|
||||
continue
|
||||
data = line[6:]
|
||||
if data.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
content = chunk["choices"][0]["delta"].get("content", "")
|
||||
if content:
|
||||
out_file.write(content)
|
||||
except Exception as e:
|
||||
print("Error parsing chunk:", e)
|
||||
|
||||
def main():
|
||||
if not os.path.exists(README_PATH):
|
||||
print("README.md not found!")
|
||||
return
|
||||
|
||||
with open(README_PATH, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
payload = {
|
||||
"model": API_MODEL,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a translation assistant that translates English Markdown text to Chinese. Preserve original Markdown formatting and line breaks."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": content
|
||||
}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
print("Streaming translation started...")
|
||||
stream_translation(API_URL, API_KEY, payload)
|
||||
print(f"Translation completed and saved to {OUTPUT_PATH}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
EOF
|
||||
|
||||
python3 translate_readme.py
|
||||
rm -rf translate_readme.py
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.skip_translation == 'false'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update helm translated README.zh.md"
|
||||
branch: update-helm-readme-zh
|
||||
title: "Update helm translated README.zh.md"
|
||||
body: |
|
||||
This PR updates the translated README.zh.md file.
|
||||
|
||||
- Automatically generated by GitHub Actions
|
||||
labels: translation, automated
|
||||
base: main
|
||||
29
.github/workflows/translate-test.yml
vendored
Normal file
29
.github/workflows/translate-test.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'Translate GitHub content into English'
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
discussion:
|
||||
types: [created, edited]
|
||||
discussion_comment:
|
||||
types: [created, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: lizheming/github-translate-action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
APPEND_TRANSLATION: true
|
||||
@@ -27,12 +27,14 @@ header:
|
||||
- 'plugins/**'
|
||||
- 'CODEOWNERS'
|
||||
- 'VERSION'
|
||||
- 'DEP_VERSION'
|
||||
- 'tools/'
|
||||
- 'test/README.md'
|
||||
- 'test/README_CN.md'
|
||||
- 'hgctl/cmd/hgctl/config/testdata/config'
|
||||
- 'hgctl/pkg/manifests'
|
||||
- 'pkg/ingress/kube/gateway/istio/testdata'
|
||||
- 'release-notes/**'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/envoy @gengleilei @johnlanni
|
||||
/istio @SpecialYang @johnlanni
|
||||
/pkg @SpecialYang @johnlanni @CH3CHO
|
||||
/plugins @johnlanni @CH3CHO @rinfx
|
||||
/plugins/wasm-go/extensions/ai-proxy @cr7258 @CH3CHO @rinfx
|
||||
/plugins @johnlanni @CH3CHO @rinfx @erasernoob
|
||||
/plugins/wasm-go/extensions/ai-proxy @cr7258 @CH3CHO @rinfx @wydream
|
||||
/plugins/wasm-rust @007gzs @jizhuozhi
|
||||
/registry @NameHaibinZhang @2456868764 @johnlanni
|
||||
/registry @Erica177 @2456868764 @johnlanni
|
||||
/test @Xunzhuo @2456868764 @CH3CHO
|
||||
/tools @johnlanni @Xunzhuo @2456868764
|
||||
|
||||
|
||||
1
DEP_VERSION
Normal file
1
DEP_VERSION
Normal file
@@ -0,0 +1 @@
|
||||
higress-console: v2.1.6
|
||||
@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.5/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.8/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -191,8 +191,9 @@ 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 ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
|
||||
ISTIO_LATEST_IMAGE_TAG ?= d9c728d3b01f64855e012b08d136e306f1160397
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= latest
|
||||
ISTIO_LATEST_IMAGE_TAG ?= latest
|
||||
|
||||
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'
|
||||
@@ -268,10 +269,26 @@ higress-conformance-test-clean: $(tools/kind) delete-cluster
|
||||
.PHONY: higress-wasmplugin-test-prepare
|
||||
higress-wasmplugin-test-prepare: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin
|
||||
|
||||
# higress-wasmplugin-test-prepare-skip-docker-build prepares the environment for higress wasmplugin tests without build higress docker image.
|
||||
.PHONY: higress-wasmplugin-test-prepare-skip-docker-build
|
||||
higress-wasmplugin-test-prepare-skip-docker-build: $(tools/kind) delete-cluster create-cluster prebuild
|
||||
@export TAG="$(HIGRESS_LATEST_IMAGE_TAG)" && \
|
||||
$(MAKE) kube-load-image && \
|
||||
$(MAKE) install-dev-wasmplugin
|
||||
|
||||
# higress-wasmplugin-test runs ingress wasmplugin tests.
|
||||
.PHONY: higress-wasmplugin-test
|
||||
higress-wasmplugin-test: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin run-higress-e2e-test-wasmplugin delete-cluster
|
||||
|
||||
# higress-wasmplugin-test-skip-docker-build runs ingress wasmplugin tests without build higress docker image
|
||||
.PHONY: higress-wasmplugin-test-skip-docker-build
|
||||
higress-wasmplugin-test-skip-docker-build: $(tools/kind) delete-cluster create-cluster prebuild
|
||||
@export TAG="$(HIGRESS_LATEST_IMAGE_TAG)" && \
|
||||
$(MAKE) kube-load-image && \
|
||||
$(MAKE) install-dev-wasmplugin && \
|
||||
$(MAKE) run-higress-e2e-test-wasmplugin && \
|
||||
$(MAKE) delete-cluster
|
||||
|
||||
# higress-wasmplugin-test-clean cleans the environment for higress wasmplugin tests.
|
||||
.PHONY: higress-wasmplugin-test-clean
|
||||
higress-wasmplugin-test-clean: $(tools/kind) delete-cluster
|
||||
@@ -290,8 +307,12 @@ delete-cluster: $(tools/kind) ## Delete kind cluster.
|
||||
# dubbo-provider-demo和nacos-standlone-rc3的镜像已经上传到阿里云镜像库,第一次需要先拉到本地
|
||||
# docker pull registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo:0.0.1
|
||||
# docker pull registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3:1.0.0-RC3
|
||||
# If TAG is HIGRESS_LATEST_IMAGE_TAG, means we skip building higress docker image, so we need to pull the image first.
|
||||
.PHONY: kube-load-image
|
||||
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
|
||||
@if [ "$(TAG)" = "$(HIGRESS_LATEST_IMAGE_TAG)" ]; then \
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG); \
|
||||
fi
|
||||
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)
|
||||
|
||||
10
README.md
10
README.md
@@ -10,6 +10,7 @@
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
[](https://discord.gg/tSbww9VDaM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global APIs as MCP powered by AI Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
@@ -68,6 +69,10 @@ Port descriptions:
|
||||
|
||||
> All Higress Docker images use Higress's own image repository and are not affected by Docker Hub rate limits.
|
||||
> In addition, the submission and updates of the images are protected by a security scanning mechanism (powered by Alibaba Cloud ACR), making them very secure for use in production environments.
|
||||
>
|
||||
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
|
||||
>
|
||||
> **Southeast Asia**: `higress-registry.ap-southeast-7.cr.aliyuncs.com`
|
||||
|
||||
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.io/en-us/docs/user/quickstart).
|
||||
|
||||
@@ -140,7 +145,10 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
|
||||
## Community
|
||||
|
||||
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
|
||||
Join our Discord community! This is where you can connect with developers and other enthusiastic users of Higress.
|
||||
|
||||
[](https://discord.gg/tSbww9VDaM)
|
||||
|
||||
|
||||
### Thanks
|
||||
|
||||
|
||||
@@ -250,6 +250,10 @@ spec:
|
||||
registries:
|
||||
items:
|
||||
properties:
|
||||
allowMcpServers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
authSecretName:
|
||||
type: string
|
||||
consulDatacenter:
|
||||
@@ -265,12 +269,23 @@ spec:
|
||||
type: string
|
||||
enableMCPServer:
|
||||
type: boolean
|
||||
enableScopeMcpServers:
|
||||
type: boolean
|
||||
mcpServerBaseUrl:
|
||||
type: string
|
||||
mcpServerExportDomains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
metadata:
|
||||
additionalProperties:
|
||||
properties:
|
||||
innerMap:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
nacosAccessKey:
|
||||
type: string
|
||||
nacosAddressServer:
|
||||
|
||||
@@ -111,28 +111,31 @@ type RegistryConfig struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
|
||||
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
|
||||
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
|
||||
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
|
||||
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
|
||||
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
|
||||
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
|
||||
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
|
||||
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
|
||||
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
|
||||
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"`
|
||||
McpServerExportDomains []string `protobuf:"bytes,20,rep,name=mcpServerExportDomains,proto3" json:"mcpServerExportDomains,omitempty"`
|
||||
McpServerBaseUrl string `protobuf:"bytes,21,opt,name=mcpServerBaseUrl,proto3" json:"mcpServerBaseUrl,omitempty"`
|
||||
EnableMCPServer *wrappers.BoolValue `protobuf:"bytes,22,opt,name=enableMCPServer,proto3" json:"enableMCPServer,omitempty"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
|
||||
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
|
||||
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
|
||||
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
|
||||
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
|
||||
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
|
||||
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
|
||||
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
|
||||
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
|
||||
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
|
||||
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"`
|
||||
McpServerExportDomains []string `protobuf:"bytes,20,rep,name=mcpServerExportDomains,proto3" json:"mcpServerExportDomains,omitempty"`
|
||||
McpServerBaseUrl string `protobuf:"bytes,21,opt,name=mcpServerBaseUrl,proto3" json:"mcpServerBaseUrl,omitempty"`
|
||||
EnableMCPServer *wrappers.BoolValue `protobuf:"bytes,22,opt,name=enableMCPServer,proto3" json:"enableMCPServer,omitempty"`
|
||||
EnableScopeMcpServers *wrappers.BoolValue `protobuf:"bytes,23,opt,name=enableScopeMcpServers,proto3" json:"enableScopeMcpServers,omitempty"`
|
||||
AllowMcpServers []string `protobuf:"bytes,24,rep,name=allowMcpServers,proto3" json:"allowMcpServers,omitempty"`
|
||||
Metadata map[string]*InnerMap `protobuf:"bytes,25,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) Reset() {
|
||||
@@ -321,6 +324,74 @@ func (x *RegistryConfig) GetEnableMCPServer() *wrappers.BoolValue {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetEnableScopeMcpServers() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.EnableScopeMcpServers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetAllowMcpServers() []string {
|
||||
if x != nil {
|
||||
return x.AllowMcpServers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetMetadata() map[string]*InnerMap {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InnerMap struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
InnerMap map[string]string `protobuf:"bytes,1,rep,name=inner_map,json=innerMap,proto3" json:"inner_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *InnerMap) Reset() {
|
||||
*x = InnerMap{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *InnerMap) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*InnerMap) ProtoMessage() {}
|
||||
|
||||
func (x *InnerMap) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use InnerMap.ProtoReflect.Descriptor instead.
|
||||
func (*InnerMap) Descriptor() ([]byte, []int) {
|
||||
return file_networking_v1_mcp_bridge_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *InnerMap) GetInnerMap() map[string]string {
|
||||
if x != nil {
|
||||
return x.InnerMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_networking_v1_mcp_bridge_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
@@ -338,7 +409,7 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
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, 0xfd, 0x06, 0x0a, 0x0e, 0x52,
|
||||
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xa8, 0x09, 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, 0x18, 0x02,
|
||||
@@ -394,11 +465,39 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 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,
|
||||
0x65, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x50, 0x0a, 0x15, 0x65, 0x6e,
|
||||
0x61, 0x62, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x73, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x63, 0x6f,
|
||||
0x70, 0x65, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x0f,
|
||||
0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
|
||||
0x18, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x63, 0x70, 0x53,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x4f, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
||||
0x74, 0x61, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 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, 0x2e,
|
||||
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d,
|
||||
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x5c, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61,
|
||||
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x69, 0x67, 0x72,
|
||||
0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x93, 0x01, 0x0a, 0x08, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d,
|
||||
0x61, 0x70, 0x12, 0x4a, 0x0a, 0x09, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x70, 0x18,
|
||||
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e,
|
||||
0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x2e, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x1a, 0x3b,
|
||||
0x0a, 0x0d, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
|
||||
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
|
||||
0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 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 (
|
||||
@@ -413,20 +512,27 @@ func file_networking_v1_mcp_bridge_proto_rawDescGZIP() []byte {
|
||||
return file_networking_v1_mcp_bridge_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_networking_v1_mcp_bridge_proto_goTypes = []interface{}{
|
||||
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
|
||||
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
|
||||
(*wrappers.BoolValue)(nil), // 2: google.protobuf.BoolValue
|
||||
(*InnerMap)(nil), // 2: higress.networking.v1.InnerMap
|
||||
nil, // 3: higress.networking.v1.RegistryConfig.MetadataEntry
|
||||
nil, // 4: higress.networking.v1.InnerMap.InnerMapEntry
|
||||
(*wrappers.BoolValue)(nil), // 5: google.protobuf.BoolValue
|
||||
}
|
||||
var file_networking_v1_mcp_bridge_proto_depIdxs = []int32{
|
||||
1, // 0: higress.networking.v1.McpBridge.registries:type_name -> higress.networking.v1.RegistryConfig
|
||||
2, // 1: higress.networking.v1.RegistryConfig.enableMCPServer:type_name -> google.protobuf.BoolValue
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
5, // 1: higress.networking.v1.RegistryConfig.enableMCPServer:type_name -> google.protobuf.BoolValue
|
||||
5, // 2: higress.networking.v1.RegistryConfig.enableScopeMcpServers:type_name -> google.protobuf.BoolValue
|
||||
3, // 3: higress.networking.v1.RegistryConfig.metadata:type_name -> higress.networking.v1.RegistryConfig.MetadataEntry
|
||||
4, // 4: higress.networking.v1.InnerMap.inner_map:type_name -> higress.networking.v1.InnerMap.InnerMapEntry
|
||||
2, // 5: higress.networking.v1.RegistryConfig.MetadataEntry.value:type_name -> higress.networking.v1.InnerMap
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_networking_v1_mcp_bridge_proto_init() }
|
||||
@@ -459,6 +565,18 @@ func file_networking_v1_mcp_bridge_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_networking_v1_mcp_bridge_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*InnerMap); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -466,7 +584,7 @@ func file_networking_v1_mcp_bridge_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_networking_v1_mcp_bridge_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -71,4 +71,11 @@ message RegistryConfig {
|
||||
repeated string mcpServerExportDomains = 20;
|
||||
string mcpServerBaseUrl = 21;
|
||||
google.protobuf.BoolValue enableMCPServer = 22;
|
||||
google.protobuf.BoolValue enableScopeMcpServers = 23;
|
||||
repeated string allowMcpServers = 24;
|
||||
map<string, InnerMap> metadata = 25;
|
||||
}
|
||||
|
||||
message InnerMap {
|
||||
map<string, string> inner_map = 1;
|
||||
}
|
||||
@@ -46,3 +46,24 @@ func (in *RegistryConfig) DeepCopy() *RegistryConfig {
|
||||
func (in *RegistryConfig) DeepCopyInterface() interface{} {
|
||||
return in.DeepCopy()
|
||||
}
|
||||
|
||||
// DeepCopyInto supports using InnerMap within kubernetes types, where deepcopy-gen is used.
|
||||
func (in *InnerMap) DeepCopyInto(out *InnerMap) {
|
||||
p := proto.Clone(in).(*InnerMap)
|
||||
*out = *p
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InnerMap. Required by controller-gen.
|
||||
func (in *InnerMap) DeepCopy() *InnerMap {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(InnerMap)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new InnerMap. Required by controller-gen.
|
||||
func (in *InnerMap) DeepCopyInterface() interface{} {
|
||||
return in.DeepCopy()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,17 @@ func (this *RegistryConfig) UnmarshalJSON(b []byte) error {
|
||||
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
|
||||
}
|
||||
|
||||
// MarshalJSON is a custom marshaler for InnerMap
|
||||
func (this *InnerMap) MarshalJSON() ([]byte, error) {
|
||||
str, err := McpBridgeMarshaler.MarshalToString(this)
|
||||
return []byte(str), err
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a custom unmarshaler for InnerMap
|
||||
func (this *InnerMap) UnmarshalJSON(b []byte) error {
|
||||
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
|
||||
}
|
||||
|
||||
var (
|
||||
McpBridgeMarshaler = &jsonpb.Marshaler{}
|
||||
McpBridgeUnmarshaler = &jsonpb.Unmarshaler{AllowUnknownFields: true}
|
||||
|
||||
Submodule envoy/envoy updated: 17cf01d9f6...e2707255f1
4
go.mod
4
go.mod
@@ -31,7 +31,7 @@ require (
|
||||
github.com/hudl/fargo v1.4.0
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2
|
||||
github.com/onsi/gomega v1.27.10
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -202,6 +202,7 @@ require (
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
@@ -274,6 +275,5 @@ replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0
|
||||
|
||||
replace (
|
||||
github.com/dubbogo/gost => github.com/johnlanni/gost v1.11.23-0.20220713132522-0967a24036c6
|
||||
github.com/nacos-group/nacos-sdk-go/v2 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60
|
||||
golang.org/x/exp => golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1434,8 +1434,6 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60 h1:FA/azfz2nSkMc1XR8LeqhcAiA/2/sOMcyBGYCTUc+Cs=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
|
||||
@@ -1525,6 +1523,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8 h1:8pEm05Cdav9sQgJSv5kyvlgfz0SzFUUGI3pWX6SiSnM=
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2 h1:9QB2nCJzT5wkTVlxNYl3XL/7+G6p2USMi2gQh/ouQQo=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.2
|
||||
appVersion: 2.1.6
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -15,4 +15,4 @@ dependencies:
|
||||
repository: "file://../redis"
|
||||
version: 0.0.1
|
||||
type: application
|
||||
version: 2.1.2
|
||||
version: 2.1.6
|
||||
|
||||
@@ -250,6 +250,10 @@ spec:
|
||||
registries:
|
||||
items:
|
||||
properties:
|
||||
allowMcpServers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
authSecretName:
|
||||
type: string
|
||||
consulDatacenter:
|
||||
@@ -263,6 +267,25 @@ spec:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
enableMCPServer:
|
||||
type: boolean
|
||||
enableScopeMcpServers:
|
||||
type: boolean
|
||||
mcpServerBaseUrl:
|
||||
type: string
|
||||
mcpServerExportDomains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
metadata:
|
||||
additionalProperties:
|
||||
properties:
|
||||
innerMap:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
nacosAccessKey:
|
||||
type: string
|
||||
nacosAddressServer:
|
||||
|
||||
@@ -113,3 +113,36 @@ kind: VMPodScrape
|
||||
{{- fail "unexpected gateway.metrics.provider" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "pluginServer.name" -}}
|
||||
{{- .Values.pluginServer.name | default "higress-plugin-server" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "pluginServer.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "pluginServer.labels" -}}
|
||||
helm.sh/chart: {{ include "pluginServer.chart" . }}
|
||||
{{ include "pluginServer.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/name: {{ include "pluginServer.name" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "pluginServer.selectorLabels" -}}
|
||||
{{- if hasKey .Values.pluginServer.labels "app" }}
|
||||
{{- with .Values.pluginServer.labels.app }}app: {{.|quote}}
|
||||
{{- end}}
|
||||
{{- else }}app: {{ include "pluginServer.name" . }}
|
||||
{{- end }}
|
||||
{{- if hasKey .Values.pluginServer.labels "higress" }}
|
||||
{{- with .Values.pluginServer.labels.higress }}
|
||||
higress: {{.|quote}}
|
||||
{{- end}}
|
||||
{{- else }}
|
||||
higress: {{ include "pluginServer.name" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
accessLogFile: "/dev/stdout"
|
||||
{{- end }}
|
||||
ingressControllerMode: "OFF"
|
||||
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
|
||||
|
||||
'
|
||||
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}'
|
||||
dnsRefreshRate: 200s
|
||||
enableAutoMtls: false
|
||||
enablePrometheusMerge: false
|
||||
@@ -99,7 +97,7 @@ metadata:
|
||||
name: higress-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
data:
|
||||
higress: |-
|
||||
{{- $existingConfig := lookup "v1" "ConfigMap" .Release.Namespace "higress-config" }}
|
||||
@@ -126,7 +124,7 @@ data:
|
||||
{{- else }}
|
||||
networks: {}
|
||||
{{- end }}
|
||||
|
||||
|
||||
mesh: |-
|
||||
{{- if .Values.meshConfig }}
|
||||
{{ $mesh | toYaml | indent 4 }}
|
||||
|
||||
@@ -6,4 +6,8 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "controller.labels" . | nindent 4 }}
|
||||
{{- with .Values.controller.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
39
helm/core/templates/plugin-server-deployment.yaml
Normal file
39
helm/core/templates/plugin-server-deployment.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
{{- if .Values.global.enablePluginServer }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "pluginServer.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
replicas: {{ .Values.pluginServer.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "pluginServer.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- with .Values.pluginServer.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "pluginServer.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.pluginServer.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: {{ .Values.pluginServer.hub | default .Values.global.hub }}/{{ .Values.pluginServer.image | default "plugin-server" }}:{{ .Values.pluginServer.tag | default "1.0.0" }}
|
||||
{{- if .Values.global.imagePullPolicy }}
|
||||
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.pluginServer.resources.requests.cpu }}
|
||||
memory: {{ .Values.pluginServer.resources.requests.memory }}
|
||||
limits:
|
||||
cpu: {{ .Values.pluginServer.resources.limits.cpu }}
|
||||
memory: {{ .Values.pluginServer.resources.limits.memory }}
|
||||
{{- end }}
|
||||
16
helm/core/templates/plugin-server-service.yaml
Normal file
16
helm/core/templates/plugin-server-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
{{- if .Values.global.enablePluginServer }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "pluginServer.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "pluginServer.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.pluginServer.service.port }}
|
||||
targetPort: 8080
|
||||
selector:
|
||||
{{- include "pluginServer.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -11,6 +11,7 @@ global:
|
||||
enableSRDS: true
|
||||
# -- Whether to enable Redis(redis-stack-server) for Higress, default is false.
|
||||
enableRedis: false
|
||||
enablePluginServer: false
|
||||
onDemandRDS: false
|
||||
hostRDSMergeSubset: false
|
||||
onlyPushRouteCluster: true
|
||||
@@ -580,8 +581,7 @@ controller:
|
||||
# -- Labels to apply to the pod
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext:
|
||||
{}
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
ports:
|
||||
@@ -708,13 +708,13 @@ tracing:
|
||||
enable: false
|
||||
sampling: 100
|
||||
timeout: 500
|
||||
skywalking:
|
||||
# access_token: ""
|
||||
service: ""
|
||||
port: 11800
|
||||
# skywalking:
|
||||
# access_token: ""
|
||||
# service: ""
|
||||
# port: 11800
|
||||
# zipkin:
|
||||
# service: ""
|
||||
# port: 9411
|
||||
# service: ""
|
||||
# port: 9411
|
||||
|
||||
# -- Downstream config settings
|
||||
downstream:
|
||||
@@ -767,4 +767,31 @@ redis:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
# -- Persistent Volume size
|
||||
size: 1Gi
|
||||
size: 1Gi
|
||||
|
||||
pluginServer:
|
||||
name: "higress-plugin-server"
|
||||
# -- Number of Higress Plugin Server pods, 2 recommended for high availability
|
||||
replicas: 2
|
||||
image: plugin-server
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
labels: {}
|
||||
# -- Labels to apply to the pod
|
||||
podLabels: {}
|
||||
|
||||
# Plugin-server Service configuration
|
||||
service:
|
||||
port: 80 # Container target port (usually fixed)
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.2
|
||||
version: 2.1.6
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.2
|
||||
digest: sha256:7612de239141ca0d27400f7d5b9a786acd98826f511e2e3ed65ccd9d2c9f1700
|
||||
generated: "2025-04-29T20:52:39.996652+08:00"
|
||||
version: 2.1.6
|
||||
digest: sha256:c5bebb3bd92bf799804443faf9ab69e88ed26815a709e58911859b504b3d04db
|
||||
generated: "2025-07-30T21:13:57.834398+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.2
|
||||
appVersion: 2.1.6
|
||||
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.1.2
|
||||
version: 2.1.6
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.2
|
||||
version: 2.1.6
|
||||
type: application
|
||||
version: 2.1.2
|
||||
version: 2.1.6
|
||||
|
||||
@@ -165,6 +165,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableLDSCache | bool | `false` | |
|
||||
| global.enablePluginServer | bool | `false` | |
|
||||
| global.enableProxyProtocol | bool | `false` | |
|
||||
| global.enablePushAllMCPClusters | bool | `true` | |
|
||||
| global.enableRedis | bool | `false` | Whether to enable Redis(redis-stack-server) for Higress, default is false. |
|
||||
@@ -273,6 +274,19 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| pilot.serviceAnnotations | object | `{}` | |
|
||||
| pilot.tag | string | `""` | |
|
||||
| pilot.traceSampling | float | `1` | |
|
||||
| pluginServer.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| pluginServer.image | string | `"plugin-server"` | |
|
||||
| pluginServer.imagePullSecrets | list | `[]` | |
|
||||
| pluginServer.labels | object | `{}` | |
|
||||
| pluginServer.name | string | `"higress-plugin-server"` | |
|
||||
| pluginServer.podLabels | object | `{}` | Labels to apply to the pod |
|
||||
| pluginServer.replicas | int | `2` | Number of Higress Plugin Server pods, 2 recommended for high availability |
|
||||
| pluginServer.resources.limits.cpu | string | `"500m"` | |
|
||||
| pluginServer.resources.limits.memory | string | `"256Mi"` | |
|
||||
| pluginServer.resources.requests.cpu | string | `"200m"` | |
|
||||
| pluginServer.resources.requests.memory | string | `"128Mi"` | |
|
||||
| pluginServer.service.port | int | `80` | |
|
||||
| pluginServer.tag | string | `""` | |
|
||||
| redis.redis.affinity | object | `{}` | Affinity for Redis |
|
||||
| redis.redis.image | string | `"redis-stack-server"` | Specify the image |
|
||||
| redis.redis.name | string | `"redis-stack-server"` | |
|
||||
@@ -292,7 +306,5 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| 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 |
|
||||
@@ -1,8 +1,8 @@
|
||||
## Higress for Kubernetes
|
||||
## Higress 适用于 Kubernetes
|
||||
|
||||
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
|
||||
|
||||
通过基于 Istio 和 Envoy,Higress 实现了流量网关、微服务网关和安全网关的三重网关架构的集成,从而大大降低了部署、运维的成本。
|
||||
通过 Istio 和 Envoy 的支持,Higress 实现了流量网关、微服务网关和安全网关三种架构的融合,从而极大地减少了部署、运维的成本。
|
||||
|
||||
## 设置仓库信息
|
||||
|
||||
@@ -13,7 +13,7 @@ helm repo update
|
||||
|
||||
## 安装
|
||||
|
||||
使用名为 `higress` 的版本来安装 chart:
|
||||
使用 Helm 安装名为 `higress` 的组件:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
@@ -21,60 +21,130 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
|
||||
|
||||
## 卸载
|
||||
|
||||
卸载删除 higress 部署:
|
||||
删除名称为 higress 的安装:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
该命令会删除与 chart 相关的所有 Kubernetes 组件并删除发行版。
|
||||
该命令将删除与组件关联的所有 Kubernetes 组件并卸载该发行版。
|
||||
|
||||
## 参数
|
||||
|
||||
## 配置值
|
||||
## Values
|
||||
|
||||
| 键名 | 类型 | 默认值 | 描述 |
|
||||
|------|------|---------|-------------|
|
||||
| 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.podLabels | object | `{}` | 应用到 pod 上的标签 |
|
||||
| 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.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` | 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"` | |
|
||||
| gateway.metrics.enabled | bool | `false` | 如果为 true,则为gateway创建PodMonitor或VMPodScrape |
|
||||
| gateway.metrics.provider | string | `monitoring.coreos.com` | CustomResourceDefinition 的提供商组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
|
||||
| gateway.readinessFailureThreshold | int | `30` | 成功进行探针测试前连续失败探针的最大次数。 |
|
||||
| global MeshNetworks | object | `{}` | |
|
||||
| global.tracer.datadog.address | string | `"$(HOST_IP):8126"` | 提交给 Datadog agent 的 Host:Port 。|
|
||||
| redis.redis.persistence.enabled | bool | `false` | 启用 Redis 持久性,默认为 false |
|
||||
| redis.redis.persistence.size | string | `"1Gi"` | Persistent Volume 大小 |
|
||||
| redis.redis.service.port | int | `6379` | Exporter service 端口 |
|
||||
| tracing.skywalking.port | int | `11800` | |
|
||||
| upstream.connectionBufferLimits | int | `10485760` | 上游连接缓冲限制(字节)|
|
||||
| 键 | 类型 | 默认值 | 描述 |
|
||||
|----|------|---------|-------------|
|
||||
| clusterName | string | `""` | 集群名 |
|
||||
| controller.affinity | object | `{}` | 控制器亲和性设置 |
|
||||
| controller.automaticHttps.email | string | `""` | 自动 HTTPS 所需的邮件 |
|
||||
| controller.automaticHttps.enabled | bool | `true` | 是否启用自动 HTTPS 功能 |
|
||||
| controller.autoscaling.enabled | bool | `false` | 是否启用控制器的自动扩展功能 |
|
||||
| controller.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| controller.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | 目标 CPU 使用率百分比 |
|
||||
| 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 | `{}` | Pod 注解 |
|
||||
| controller.podLabels | object | `{}` | 应用到 Pod 上的标签 |
|
||||
| controller.podSecurityContext | object | `{}` | Pod 安全上下文 |
|
||||
| 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` | 是否创建 RBAC 相关资源 |
|
||||
| controller.replicas | int | `1` | Higress 控制器 Pod 的数量 |
|
||||
| controller.resources.limits.cpu | string | `"1000m"` | CPU 上限 |
|
||||
| controller.resources.limits.memory | string | `"2048Mi"` | 内存上限 |
|
||||
| controller.resources.requests.cpu | string | `"500m"` | CPU 请求量 |
|
||||
| controller.resources.requests.memory | string | `"2048Mi"` | 内存请求量 |
|
||||
| controller.securityContext | object | `{}` | 安全上下文 |
|
||||
| controller.service.type | string | `"ClusterIP"` | 服务类型 |
|
||||
| controller.serviceAccount.annotations | object | `{}` | 添加到服务帐户的注解 |
|
||||
| controller.serviceAccount.create | bool | `true` | 是否创建服务帐户 |
|
||||
| controller.serviceAccount.name | string | `""` | 如果未设置且 create 为 true,则从 fullname 模板生成名称 |
|
||||
| controller.tag | string | `""` | 标记 |
|
||||
| controller.tolerations | list | `[]` | 受容容忍度列表 |
|
||||
| downstream.connectionBufferLimits | int | `32768` | 下游连接缓冲区限制(字节) |
|
||||
| downstream.http2.initialConnectionWindowSize | int | `1048576` | HTTP/2 初始连接窗口大小 |
|
||||
| downstream.http2.initialStreamWindowSize | int | `65535` | 流初始窗口大小 |
|
||||
| downstream.http2.maxConcurrentStreams | int | `100` | 并发流最大数量 |
|
||||
| downstream.idleTimeout | int | `180` | 空闲超时时间(秒) |
|
||||
| downstream.maxRequestHeadersKb | int | `60` | 最大请求头大小(KB) |
|
||||
| downstream.routeTimeout | int | `0` | 路由超时时间 |
|
||||
| gateway.affinity | object | `{}` | 网关的节点亲和性 |
|
||||
| gateway.annotations | object | `{}` | 应用于所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | bool | `false` | 启用网关的自动扩展功能 |
|
||||
| gateway.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| gateway.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | CPU 使用率的目标百分比 |
|
||||
| gateway.containerSecurityContext | string | `nil` | 网关容器的安全配置上下文 |
|
||||
| gateway.env | object | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | bool | `false` | 是否使用主机网络 |
|
||||
| gateway.httpPort | int | `80` | HTTP 服务端口 |
|
||||
| gateway.httpsPort | int | `443` | HTTPS 服务端口 |
|
||||
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 网关镜像的基础域名 |
|
||||
| gateway.image | string | `"gateway"` | |
|
||||
| gateway.kind | string | `"Deployment"` | 部署类型 |
|
||||
| gateway.labels | object | `{}` | 应用于所有资源的标签 |
|
||||
| gateway.metrics.enabled | bool | `false` | 启用网关度量收集 |
|
||||
| gateway.metrics.honorLabels | bool | `false` | 是否合并现有标签 |
|
||||
| gateway.metrics.interval | string | `""` | 度量间隔时间 |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | 定义监控提供者 |
|
||||
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
|
||||
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
|
||||
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
|
||||
| gateway.name | string | `"higress-gateway"` | 网关名称 |
|
||||
| gateway.networkGateway | string | `""` | 网络网关指定 |
|
||||
| gateway.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| gateway.replicas | int | `2` | Higress Gateway pod 的数量 |
|
||||
| gateway.resources.limits.cpu | string | `"2000m"` | 容器资源限制的 CPU |
|
||||
| gateway.resources.limits.memory | string | `"2048Mi"` | 容器资源限制的内存 |
|
||||
| gateway.resources.requests.cpu | string | `"2000m"` | 容器资源请求的 CPU |
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | 容器资源请求的内存 |
|
||||
| gateway.revision | string | `""` | 网关所属版本声明 |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | 最大激增数目百分比 |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | 最大不可用比例 |
|
||||
| gateway.readinessFailureThreshold | int | `30` | 成功尝试之前连续失败的最大探测次数 |
|
||||
| gateway.readinessInitialDelaySeconds | int | `1` | 初次检测推迟多少秒后开始探测存活状态 |
|
||||
| gateway.readinessPeriodSeconds | int | `2` | 存活探测间隔秒数 |
|
||||
| gateway.readinessSuccessThreshold | int | `1` | 认为成功之前连续成功最小探测次数 |
|
||||
| gateway.readinessTimeoutSeconds | int | `3` | 存活探测超时秒数 |
|
||||
| gateway.securityContext | string | `nil` | 客户豆荚的安全上下文 |
|
||||
| gateway.service.annotations | object | `{}` | 应用于服务账户的注释 |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | 外部路由策略 |
|
||||
| gateway.service.loadBalancerClass | string | `""` | 负载均衡器类别 |
|
||||
| gateway.service.loadBalancerIP | string | `""` | 负载均衡器 IP 地址 |
|
||||
| gateway.service.loadBalancerSourceRanges | list | `[]` | 允许访问负载均衡器的 CIDR 范围 |
|
||||
| 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"` | 服务类型 |
|
||||
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
由于内容较多,其他参数可以参考完整表。
|
||||
|
||||
@@ -33,7 +33,8 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
logs "github.com/higress-group/wasm-go/pkg/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -72,13 +73,13 @@ type PluginConfig struct {
|
||||
secondField string ` + "`required:\"true\"`" + `
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *PluginConfig, log wrapper.Log) error {
|
||||
func parseConfig(json gjson.Result, config *PluginConfig, log logs.Log) error {
|
||||
config.firstField = json.Get("firstField").String()
|
||||
config.secondField = json.Get("secondField").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log logs.Log) types.Action {
|
||||
err := proxywasm.AddHttpRequestHeader(config.firstField, config.secondField)
|
||||
if err != nil {
|
||||
log.Critical("failed to set request header")
|
||||
@@ -90,10 +91,10 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
|
||||
|
||||
module {{ .Name }}
|
||||
|
||||
go 1.19
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go main
|
||||
github.com/higress-group/wasm-go main
|
||||
github.com/higress-group/proxy-wasm-go-sdk main
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
Submodule istio/istio updated: 1492505b14...fa8896cf33
@@ -63,6 +63,7 @@ import (
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/ingress"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/ingressv1"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpbridge"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/wasmplugin"
|
||||
@@ -158,6 +159,8 @@ type IngressConfig struct {
|
||||
|
||||
// secretConfigMgr manages secret dependencies
|
||||
secretConfigMgr *SecretConfigMgr
|
||||
|
||||
mcpServerCache mcpserver.McpServerCache
|
||||
}
|
||||
|
||||
// getSecretValue implements the getValue function for secret references
|
||||
@@ -224,6 +227,7 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
|
||||
|
||||
higressConfigController := configmap.NewController(localKubeClient, clusterId, namespace)
|
||||
config.configmapMgr = configmap.NewConfigmapMgr(xdsUpdater, namespace, higressConfigController, higressConfigController.Lister())
|
||||
config.configmapMgr.RegisterMcpServerProvider(&config.mcpServerCache)
|
||||
|
||||
httpsConfigMgr, _ := cert.NewConfigMgr(namespace, localKubeClient.Kube())
|
||||
config.httpsConfigMgr = httpsConfigMgr
|
||||
@@ -421,6 +425,10 @@ func (m *IngressConfig) createWrapperConfigs(configs []config.Config) []common.W
|
||||
m.watchedSecretSet = globalContext.WatchedSecrets
|
||||
m.mutex.Unlock()
|
||||
|
||||
if m.mcpServerCache.SetMcpServers(globalContext.McpServers) {
|
||||
m.notifyXDSFullUpdate(mcpserver.GvkMcpServer, "mcp-server-annotation-change", nil)
|
||||
}
|
||||
|
||||
return wrapperConfigs
|
||||
}
|
||||
|
||||
@@ -590,7 +598,7 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
|
||||
Spec: vs,
|
||||
})
|
||||
}
|
||||
// add vs from naco3 for mcp server
|
||||
// add vs from nacos3 for mcp server
|
||||
if m.RegistryReconciler != nil {
|
||||
allConfigsFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.VirtualService)
|
||||
for _, cfg := range allConfigsFromMcp {
|
||||
@@ -794,23 +802,38 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
|
||||
if !exist {
|
||||
destinationRules[serviceName] = destinationRuleWrapper
|
||||
} else if dr.DestinationRule.TrafficPolicy != nil {
|
||||
if dr.DestinationRule.TrafficPolicy.LoadBalancer == nil &&
|
||||
destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer != nil {
|
||||
dr.DestinationRule.TrafficPolicy.LoadBalancer = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer
|
||||
}
|
||||
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 the service is referenced by an sse type mcp server, an source ip based consistent hashing policy needs to be configured
|
||||
// consistent hashing policy will be generated by mcp server watcher, then if service do not have LoadBalancer settings, it will be merged
|
||||
if destinationRuleWrapper.DestinationRule.TrafficPolicy != nil && destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer != nil {
|
||||
if dr.DestinationRule.TrafficPolicy.LoadBalancer == nil {
|
||||
dr.DestinationRule.TrafficPolicy.LoadBalancer = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer
|
||||
} else if dr.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy == nil {
|
||||
dr.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy
|
||||
}
|
||||
}
|
||||
if portUpdated {
|
||||
continue
|
||||
// if the service is referenced by an https type mcp server, an client side simple mode tls policy needs to be configured
|
||||
// simple mode tls policy will be generated by mcp server watcher, then if service do not have tls settings, it will be merged
|
||||
if dr.DestinationRule.TrafficPolicy.Tls == nil && destinationRuleWrapper.DestinationRule.TrafficPolicy != nil &&
|
||||
destinationRuleWrapper.DestinationRule.TrafficPolicy.Tls != nil {
|
||||
dr.DestinationRule.TrafficPolicy.Tls = destinationRuleWrapper.DestinationRule.TrafficPolicy.Tls
|
||||
}
|
||||
// Directly inherit or override the port policy (if it exists)
|
||||
if len(destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings) > 0 {
|
||||
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
|
||||
policy.LoadBalancer = portTrafficPolicy.LoadBalancer
|
||||
portUpdated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if portUpdated {
|
||||
continue
|
||||
}
|
||||
dr.DestinationRule.TrafficPolicy.PortLevelSettings = append(dr.DestinationRule.TrafficPolicy.PortLevelSettings, portTrafficPolicy)
|
||||
}
|
||||
dr.DestinationRule.TrafficPolicy.PortLevelSettings = append(dr.DestinationRule.TrafficPolicy.PortLevelSettings, portTrafficPolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1208,9 +1231,9 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
f(config.Config{Meta: efMetadata}, config.Config{Meta: efMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace, m.clusterId.String())
|
||||
m.configmapMgr.RegisterMcpServerProvider(m.RegistryReconciler)
|
||||
}
|
||||
reconciler := m.RegistryReconciler
|
||||
m.configmapMgr.SetMcpReconciler(m.RegistryReconciler)
|
||||
err = reconciler.Reconcile(mcpbridge)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Mcpbridge reconcile failed, err:%v", err)
|
||||
@@ -1776,3 +1799,19 @@ func (m *IngressConfig) Patch(config.Config, config.PatchFunc) (string, error) {
|
||||
func (m *IngressConfig) Delete(config.GroupVersionKind, string, string, *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(gvk config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
var configsUpdated map[istiomodel.ConfigKey]struct{}
|
||||
if updatedConfigName != nil {
|
||||
configsUpdated = map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Name: updatedConfigName.Name,
|
||||
Namespace: updatedConfigName.Namespace,
|
||||
}: {}}
|
||||
}
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: configsUpdated,
|
||||
Reason: istiomodel.NewReasonStats(reason),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
)
|
||||
|
||||
type GlobalContext struct {
|
||||
@@ -30,6 +32,8 @@ type GlobalContext struct {
|
||||
ClusterSecretLister map[cluster.ID]listersv1.SecretLister
|
||||
|
||||
ClusterServiceList map[cluster.ID]listersv1.ServiceLister
|
||||
|
||||
McpServers []*mcpserver.McpServer
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
@@ -169,6 +173,7 @@ func NewAnnotationHandlerManager() AnnotationHandler {
|
||||
match{},
|
||||
headerControl{},
|
||||
http2rpc{},
|
||||
mcpServer{},
|
||||
},
|
||||
gatewayHandlers: []GatewayHandler{
|
||||
downstreamTLS{},
|
||||
|
||||
94
pkg/ingress/kube/annotations/mcpserver.go
Normal file
94
pkg/ingress/kube/annotations/mcpserver.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
enableMcpServer = "mcp-server"
|
||||
mcpServerMatchRuleDomains = "mcp-server-match-rule-domains"
|
||||
mcpServerMatchRuleType = "mcp-server-match-rule-type"
|
||||
mcpServerMatchRuleValue = "mcp-server-match-rule-value"
|
||||
mcpServerUpstreamType = "mcp-server-upstream-type"
|
||||
mcpServerEnablePathRewrite = "mcp-server-enable-path-rewrite"
|
||||
mcpServerPathRewritePrefix = "mcp-server-path-rewrite-prefix"
|
||||
)
|
||||
|
||||
// help to conform mcpServer implements method of Parse
|
||||
var (
|
||||
_ Parser = &mcpServer{}
|
||||
)
|
||||
|
||||
type mcpServer struct{}
|
||||
|
||||
func (a mcpServer) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if globalContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ingressKey := config.Namespace + "/" + config.Name
|
||||
|
||||
enabled, _ := annotations.ParseBoolASAP(enableMcpServer)
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var matchRuleDomains []string
|
||||
rawMatchRuleDomains, _ := annotations.ParseStringASAP(mcpServerMatchRuleDomains)
|
||||
if rawMatchRuleDomains == "" || rawMatchRuleDomains == "*" {
|
||||
// Match all domains. Leave an empty slice.
|
||||
} else if strings.Contains(rawMatchRuleDomains, ",") {
|
||||
matchRuleDomains = strings.Split(rawMatchRuleDomains, ",")
|
||||
} else {
|
||||
matchRuleDomains = []string{rawMatchRuleDomains}
|
||||
}
|
||||
|
||||
matchRuleType, _ := annotations.ParseStringASAP(mcpServerMatchRuleType)
|
||||
if matchRuleType == "" {
|
||||
log.IngressLog.Errorf("ingress %s: mcp-server-match-rule-path-type is empty", ingressKey)
|
||||
return nil
|
||||
} else if !mcpserver.ValidPathMatchTypes[matchRuleType] {
|
||||
log.IngressLog.Errorf("ingress %s: mcp-server-match-rule-path-type %s is not supported", ingressKey, matchRuleType)
|
||||
return nil
|
||||
}
|
||||
|
||||
matchRuleValue, _ := annotations.ParseStringASAP(mcpServerMatchRuleValue)
|
||||
|
||||
upstreamType, _ := annotations.ParseStringASAP(mcpServerUpstreamType)
|
||||
if upstreamType != "" && !mcpserver.ValidUpstreamTypes[upstreamType] {
|
||||
log.IngressLog.Errorf("mcp-server-upstream-type %s is not supported", upstreamType)
|
||||
return nil
|
||||
}
|
||||
|
||||
enablePathRewrite, _ := annotations.ParseBoolASAP(mcpServerEnablePathRewrite)
|
||||
pathRewritePrefix, _ := annotations.ParseStringASAP(mcpServerPathRewritePrefix)
|
||||
|
||||
globalContext.McpServers = append(globalContext.McpServers, &mcpserver.McpServer{
|
||||
Name: ingressKey,
|
||||
Domains: matchRuleDomains,
|
||||
PathMatchType: matchRuleType,
|
||||
PathMatchValue: matchRuleValue,
|
||||
UpstreamType: upstreamType,
|
||||
EnablePathRewrite: enablePathRewrite,
|
||||
PathRewritePrefix: pathRewritePrefix,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
257
pkg/ingress/kube/annotations/mcpserver_test.go
Normal file
257
pkg/ingress/kube/annotations/mcpserver_test.go
Normal file
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) 2025 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
)
|
||||
|
||||
func TestMCPServer_Parse(t *testing.T) {
|
||||
parser := mcpServer{}
|
||||
testCases := []struct {
|
||||
skip bool
|
||||
input Annotations
|
||||
expect *mcpserver.McpServer
|
||||
}{
|
||||
{
|
||||
// No annotation
|
||||
input: Annotations{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Not enabled
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "false",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but no match rule type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but empty match rule type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but bad match rule type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "bad-type",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but bad upstream type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "bad-type",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite not enabled
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite not enabled and empty domain
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: nil,
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite not enabled and wildcard domain
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "*",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: nil,
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite enabled with root
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite enabled with root
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/mcp-api",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/mcp-api",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and multiple domains
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com,www.bar.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "exact",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "sse",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com", "www.bar.com"},
|
||||
PathMatchType: "exact",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "sse",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
if tt.skip {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{Meta: Meta{
|
||||
Namespace: "default",
|
||||
Name: "route",
|
||||
}}
|
||||
globalContext := &GlobalContext{}
|
||||
_ = parser.Parse(tt.input, config, globalContext)
|
||||
if tt.expect == nil {
|
||||
if len(globalContext.McpServers) != 0 {
|
||||
t.Fatalf("globalContext.McpServers is not empty: %v", globalContext.McpServers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(globalContext.McpServers) != 1 {
|
||||
t.Fatalf("globalContext.McpServers length is not 1: %v", globalContext.McpServers)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.expect, globalContext.McpServers[0]); diff != "" {
|
||||
t.Fatalf("TestMCPServer_Parse() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
mirrorTargetService = "mirror-target-service"
|
||||
mirrorPercentage = "mirror-percentage"
|
||||
mirrorTargetService = "mirror-target-service"
|
||||
mirrorPercentage = "mirror-percentage"
|
||||
mirrorTargetFQDN = "mirror-target-fqdn"
|
||||
mirrorTargetFQDNPort = "mirror-target-fqdn-port"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,6 +36,8 @@ var (
|
||||
type MirrorConfig struct {
|
||||
util.ServiceInfo
|
||||
Percentage *wrappers.DoubleValue
|
||||
FQDN string
|
||||
FPort uint32 // Port for FQDN
|
||||
}
|
||||
|
||||
type mirror struct{}
|
||||
@@ -43,6 +47,24 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
|
||||
return nil
|
||||
}
|
||||
|
||||
// if FQDN is set, then parse FQDN
|
||||
if fqdn, err := annotations.ParseStringASAP(mirrorTargetFQDN); err == nil {
|
||||
// default is 80
|
||||
var port uint32
|
||||
port = 80
|
||||
|
||||
if p, err := annotations.ParseInt32ASAP(mirrorTargetFQDNPort); err == nil {
|
||||
port = uint32(p)
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
Percentage: parsePercentage(annotations),
|
||||
FQDN: fqdn,
|
||||
FPort: port,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
target, err := annotations.ParseStringASAP(mirrorTargetService)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Get mirror target service fail, err: %v", err)
|
||||
@@ -78,7 +100,16 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
|
||||
serviceInfo.Port = uint32(service.Spec.Ports[0].Port)
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
ServiceInfo: serviceInfo,
|
||||
Percentage: parsePercentage(annotations),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePercentage(annotations Annotations) *wrappers.DoubleValue {
|
||||
var percentage *wrappers.DoubleValue
|
||||
|
||||
if value, err := annotations.ParseIntASAP(mirrorPercentage); err == nil {
|
||||
if value < 100 {
|
||||
percentage = &wrappers.DoubleValue{
|
||||
@@ -86,12 +117,7 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
ServiceInfo: serviceInfo,
|
||||
Percentage: percentage,
|
||||
}
|
||||
return nil
|
||||
return percentage
|
||||
}
|
||||
|
||||
func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
@@ -99,10 +125,21 @@ func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
return
|
||||
}
|
||||
|
||||
var mirrorHost string
|
||||
var mirrorPort uint32
|
||||
|
||||
if config.Mirror.FQDN != "" {
|
||||
mirrorHost = config.Mirror.FQDN
|
||||
mirrorPort = config.Mirror.FPort
|
||||
} else {
|
||||
mirrorHost = util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name)
|
||||
mirrorPort = config.Mirror.Port
|
||||
}
|
||||
|
||||
route.Mirror = &networking.Destination{
|
||||
Host: util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name),
|
||||
Host: mirrorHost,
|
||||
Port: &networking.PortSelector{
|
||||
Number: config.Mirror.Port,
|
||||
Number: mirrorPort,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -114,5 +151,5 @@ func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
}
|
||||
|
||||
func needMirror(annotations Annotations) bool {
|
||||
return annotations.HasASAP(mirrorTargetService)
|
||||
return annotations.HasASAP(mirrorTargetService) || annotations.HasASAP(mirrorTargetFQDN)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
@@ -29,6 +30,28 @@ func TestParseMirror(t *testing.T) {
|
||||
expect *MirrorConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetFQDN): "www.example.com"},
|
||||
{buildNginxAnnotationKey(mirrorTargetFQDN): "www.example.com"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "www.example.com",
|
||||
FPort: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetFQDN): "192.168.252.112", buildHigressAnnotationKey(mirrorTargetFQDNPort): "8080"},
|
||||
{buildNginxAnnotationKey(mirrorTargetFQDN): "192.168.252.112", buildNginxAnnotationKey(mirrorTargetFQDNPort): "8080"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "192.168.252.112",
|
||||
FPort: 8080,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetService): "test/app"},
|
||||
@@ -149,6 +172,42 @@ func TestMirror_ApplyRoute(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Mirror: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "www.example.com",
|
||||
FPort: 80,
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Mirror: &networking.Destination{
|
||||
Host: "www.example.com",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Mirror: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "192.168.252.112",
|
||||
FPort: 8080,
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Mirror: &networking.Destination{
|
||||
Host: "192.168.252.112",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mirror := mirror{}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
@@ -33,6 +32,7 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/controller"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
@@ -59,7 +59,6 @@ type ItemController interface {
|
||||
ValidHigressConfig(higressConfig *HigressConfig) error
|
||||
ConstructEnvoyFilters() ([]*config.Config, error)
|
||||
RegisterItemEventHandler(eventHandler ItemEventHandler)
|
||||
RegisterMcpReconciler(reconciler *reconcile.Reconciler)
|
||||
}
|
||||
|
||||
type ConfigmapMgr struct {
|
||||
@@ -113,9 +112,11 @@ func (c *ConfigmapMgr) GetHigressConfig() *HigressConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConfigmapMgr) SetMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
func (c *ConfigmapMgr) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
|
||||
for _, itemController := range c.ItemControllers {
|
||||
itemController.RegisterMcpReconciler(reconciler)
|
||||
if mcpRouteProviderAware, ok := itemController.(mcpserver.McpRouteProviderAware); ok {
|
||||
mcpRouteProviderAware.RegisterMcpServerProvider(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
@@ -377,9 +376,6 @@ func (g *GlobalOptionController) RegisterItemEventHandler(eventHandler ItemEvent
|
||||
g.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (g *GlobalOptionController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
}
|
||||
|
||||
// generateDownstreamEnvoyFilter generates the downstream envoy filter.
|
||||
func (g *GlobalOptionController) generateDownstreamEnvoyFilter(downstreamValueStruct string, bufferLimitStruct string, routeTimeoutStruct string, namespace string) []*networking.EnvoyFilter_EnvoyConfigObjectPatch {
|
||||
var downstreamConfig []*networking.EnvoyFilter_EnvoyConfigObjectPatch
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
@@ -292,9 +291,6 @@ func (g *GzipController) RegisterItemEventHandler(eventHandler ItemEventHandler)
|
||||
g.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (g *GzipController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
}
|
||||
|
||||
func (g *GzipController) constructGzipStruct(gzip *Gzip, namespace string) string {
|
||||
gzipConfig := ""
|
||||
contentType := ""
|
||||
|
||||
@@ -22,12 +22,13 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
// RedisConfig defines the configuration for Redis connection
|
||||
@@ -56,7 +57,7 @@ type MCPRatelimitConfig struct {
|
||||
type SSEServer struct {
|
||||
// The name of the SSE server
|
||||
Name string `json:"name,omitempty"`
|
||||
// The path where the SSE server will be mounted, the full path is (PATH + SsePathSuffix)
|
||||
// The path where the SSE server will be mounted, the full path is (PATH + SSEPathSuffix)
|
||||
Path string `json:"path,omitempty"`
|
||||
// The type of the SSE server
|
||||
Type string `json:"type,omitempty"`
|
||||
@@ -74,6 +75,12 @@ type MatchRule struct {
|
||||
MatchRulePath string `json:"match_rule_path,omitempty"`
|
||||
// Type of match rule: exact, prefix, suffix, contains, regex
|
||||
MatchRuleType string `json:"match_rule_type,omitempty"`
|
||||
// Type of upstream(s) matched by the rule: rest (default), sse
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
// Enable request path rewrite for matched routes
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite"`
|
||||
// Prefix the request path would be rewritten to.
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix"`
|
||||
}
|
||||
|
||||
// McpServer defines the configuration for MCP (Model Context Protocol) server
|
||||
@@ -83,7 +90,7 @@ type McpServer struct {
|
||||
// Redis Config for MCP server
|
||||
Redis *RedisConfig `json:"redis,omitempty"`
|
||||
// The suffix to be appended to SSE paths, default is "/sse"
|
||||
SsePathSuffix string `json:"sse_path_suffix,omitempty"`
|
||||
SSEPathSuffix string `json:"sse_path_suffix,omitempty"`
|
||||
// List of SSE servers Configs
|
||||
Servers []*SSEServer `json:"servers,omitempty"`
|
||||
// List of match rules for filtering requests
|
||||
@@ -118,21 +125,32 @@ func validMcpServer(m *McpServer) error {
|
||||
|
||||
// Validate match rule types
|
||||
if m.MatchList != nil {
|
||||
validTypes := map[string]bool{
|
||||
validMatchRuleTypes := map[string]bool{
|
||||
"exact": true,
|
||||
"prefix": true,
|
||||
"suffix": true,
|
||||
"contains": true,
|
||||
"regex": true,
|
||||
}
|
||||
validUpstreamTypes := map[string]bool{
|
||||
"rest": true,
|
||||
"sse": true,
|
||||
"streamable": true,
|
||||
}
|
||||
|
||||
for _, rule := range m.MatchList {
|
||||
if rule.MatchRuleType == "" {
|
||||
return errors.New("match_rule_type cannot be empty, must be one of: exact, prefix, suffix, contains, regex")
|
||||
}
|
||||
if !validTypes[rule.MatchRuleType] {
|
||||
if !validMatchRuleTypes[rule.MatchRuleType] {
|
||||
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
|
||||
}
|
||||
if rule.UpstreamType != "" && !validUpstreamTypes[rule.UpstreamType] {
|
||||
return fmt.Errorf("invalid upstream_type: %s, must be one of: rest, sse, streamable", rule.UpstreamType)
|
||||
}
|
||||
if rule.EnablePathRewrite && rule.UpstreamType != "sse" {
|
||||
return errors.New("path rewrite is only supported for SSE upstream type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +192,7 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
WhiteList: mcp.Ratelimit.WhiteList,
|
||||
}
|
||||
}
|
||||
newMcp.SsePathSuffix = mcp.SsePathSuffix
|
||||
newMcp.SSEPathSuffix = mcp.SSEPathSuffix
|
||||
|
||||
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
|
||||
|
||||
@@ -201,9 +219,12 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
newMcp.MatchList = make([]*MatchRule, len(mcp.MatchList))
|
||||
for i, rule := range mcp.MatchList {
|
||||
newMcp.MatchList[i] = &MatchRule{
|
||||
MatchRuleDomain: rule.MatchRuleDomain,
|
||||
MatchRulePath: rule.MatchRulePath,
|
||||
MatchRuleType: rule.MatchRuleType,
|
||||
MatchRuleDomain: rule.MatchRuleDomain,
|
||||
MatchRulePath: rule.MatchRulePath,
|
||||
MatchRuleType: rule.MatchRuleType,
|
||||
UpstreamType: rule.UpstreamType,
|
||||
EnablePathRewrite: rule.EnablePathRewrite,
|
||||
PathRewritePrefix: rule.PathRewritePrefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,18 +233,19 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
}
|
||||
|
||||
type McpServerController struct {
|
||||
Namespace string
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
reconclier *reconcile.Reconciler
|
||||
Namespace string
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
mcpServerProviders map[mcpserver.McpServerProvider]bool
|
||||
}
|
||||
|
||||
func NewMcpServerController(namespace string) *McpServerController {
|
||||
mcpController := &McpServerController{
|
||||
Namespace: namespace,
|
||||
mcpServer: atomic.Value{},
|
||||
Name: "mcpServer",
|
||||
Namespace: namespace,
|
||||
Name: "mcpServer",
|
||||
mcpServer: atomic.Value{},
|
||||
mcpServerProviders: make(map[mcpserver.McpServerProvider]bool),
|
||||
}
|
||||
mcpController.SetMcpServer(NewDefaultMcpServer())
|
||||
return mcpController
|
||||
@@ -290,8 +312,11 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
|
||||
m.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (m *McpServerController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
m.reconclier = reconciler
|
||||
func (m *McpServerController) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
|
||||
if m.mcpServerProviders == nil {
|
||||
m.mcpServerProviders = make(map[mcpserver.McpServerProvider]bool)
|
||||
}
|
||||
m.mcpServerProviders[provider] = true
|
||||
}
|
||||
|
||||
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
@@ -386,37 +411,47 @@ func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error)
|
||||
|
||||
func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
// Build match_list configuration
|
||||
matchList := "[]"
|
||||
var matchConfigs []string
|
||||
if len(mcp.MatchList) > 0 {
|
||||
for _, rule := range mcp.MatchList {
|
||||
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
|
||||
"match_rule_domain": "%s",
|
||||
"match_rule_path": "%s",
|
||||
"match_rule_type": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType))
|
||||
var matchList []*MatchRule
|
||||
matchList = append(matchList, mcp.MatchList...)
|
||||
for provider, _ := range m.mcpServerProviders {
|
||||
servers := provider.GetMcpServers()
|
||||
if len(servers) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if m.reconclier != nil {
|
||||
vsFromMcp := m.reconclier.GetAllConfigs(gvk.VirtualService)
|
||||
for _, c := range vsFromMcp {
|
||||
vs := c.Spec.(*networking.VirtualService)
|
||||
var host string
|
||||
if len(vs.Hosts) > 1 {
|
||||
host = fmt.Sprintf("(%s)", strings.Join(vs.Hosts, "|"))
|
||||
} else {
|
||||
host = vs.Hosts[0]
|
||||
for _, server := range servers {
|
||||
matchRuleDomain := ""
|
||||
if len(server.Domains) != 0 {
|
||||
if len(server.Domains) > 1 {
|
||||
matchRuleDomain = fmt.Sprintf("(%s)", strings.Join(server.Domains, "|"))
|
||||
} else {
|
||||
matchRuleDomain = server.Domains[0]
|
||||
}
|
||||
}
|
||||
path := vs.Http[0].Match[0].Uri.GetPrefix()
|
||||
matchList = append(matchList, &MatchRule{
|
||||
MatchRuleDomain: matchRuleDomain,
|
||||
MatchRuleType: server.PathMatchType,
|
||||
MatchRulePath: server.PathMatchValue,
|
||||
UpstreamType: server.UpstreamType,
|
||||
EnablePathRewrite: server.EnablePathRewrite,
|
||||
PathRewritePrefix: server.PathRewritePrefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
matchListConfig := "[]"
|
||||
if len(matchList) > 0 {
|
||||
matchConfigs := make([]string, 0, len(matchList))
|
||||
for _, rule := range matchList {
|
||||
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
|
||||
"match_rule_domain": "%s",
|
||||
"match_rule_path": "%s",
|
||||
"match_rule_type": "prefix"
|
||||
}`, host, path))
|
||||
"match_rule_type": "%s",
|
||||
"upstream_type": "%s",
|
||||
"enable_path_rewrite": %t,
|
||||
"path_rewrite_prefix": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType, rule.UpstreamType, rule.EnablePathRewrite, rule.PathRewritePrefix))
|
||||
}
|
||||
matchListConfig = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
}
|
||||
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
|
||||
// Build redis configuration
|
||||
redisConfig := "null"
|
||||
@@ -468,8 +503,8 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
}`,
|
||||
redisConfig,
|
||||
rateLimitConfig,
|
||||
mcp.SsePathSuffix,
|
||||
matchList,
|
||||
mcp.SSEPathSuffix,
|
||||
matchListConfig,
|
||||
mcp.EnableUserLevelServer)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,61 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "enabled but bad match_rule_type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "bad-type",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("invalid match_rule_type: bad-type, must be one of: exact, prefix, suffix, contains, regex"),
|
||||
},
|
||||
{
|
||||
name: "enabled but bad upstream_type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "bad-type",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("invalid upstream_type: bad-type, must be one of: rest, sse, streamable"),
|
||||
},
|
||||
{
|
||||
name: "enabled but path rewrite with unsupported upstream type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("path rewrite is only supported for SSE upstream type"),
|
||||
},
|
||||
{
|
||||
name: "enabled with user level server but no redis config",
|
||||
mcp: &McpServer{
|
||||
@@ -76,7 +131,7 @@ func Test_validMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -238,7 +293,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -265,7 +320,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -581,13 +636,27 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
Password: "pass",
|
||||
DB: 1,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/test",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/sse-test-1",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "sse",
|
||||
},
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/sse-test-2",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "sse",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/mcp",
|
||||
},
|
||||
},
|
||||
EnableUserLevelServer: true,
|
||||
Ratelimit: &MCPRatelimitConfig{
|
||||
@@ -623,7 +692,24 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
"match_list": [{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/test",
|
||||
"match_rule_type": "exact"
|
||||
"match_rule_type": "exact",
|
||||
"upstream_type": "",
|
||||
"enable_path_rewrite": false,
|
||||
"path_rewrite_prefix": ""
|
||||
},{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/sse-test-1",
|
||||
"match_rule_type": "prefix",
|
||||
"upstream_type": "sse",
|
||||
"enable_path_rewrite": false,
|
||||
"path_rewrite_prefix": ""
|
||||
},{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/sse-test-2",
|
||||
"match_rule_type": "prefix",
|
||||
"upstream_type": "sse",
|
||||
"enable_path_rewrite": true,
|
||||
"path_rewrite_prefix": "/mcp"
|
||||
}],
|
||||
"enable_user_level_server": true
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
|
||||
@@ -238,9 +237,6 @@ func (t *TracingController) RegisterItemEventHandler(eventHandler ItemEventHandl
|
||||
t.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (t *TracingController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
}
|
||||
|
||||
func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
configs := make([]*config.Config, 0)
|
||||
tracing := t.GetTracing()
|
||||
|
||||
60
pkg/ingress/kube/mcpserver/model.go
Normal file
60
pkg/ingress/kube/mcpserver/model.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2025 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 mcpserver
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/config"
|
||||
)
|
||||
|
||||
var (
|
||||
GvkMcpServer = config.GroupVersionKind{Group: "networking.higress.io", Version: "v1alpha1", Kind: "McpServer"}
|
||||
)
|
||||
|
||||
const (
|
||||
UpstreamTypeRest string = "rest"
|
||||
UpstreamTypeSSE string = "sse"
|
||||
UpstreamTypeStreamable string = "streamable"
|
||||
|
||||
ExactMatchType string = "exact"
|
||||
PrefixMatchType string = "prefix"
|
||||
SuffixMatchType string = "suffix"
|
||||
ContainsMatchType string = "contains"
|
||||
RegexMatchType string = "regex"
|
||||
)
|
||||
|
||||
var (
|
||||
ValidUpstreamTypes = map[string]bool{
|
||||
UpstreamTypeRest: true,
|
||||
UpstreamTypeSSE: true,
|
||||
UpstreamTypeStreamable: true,
|
||||
}
|
||||
ValidPathMatchTypes = map[string]bool{
|
||||
ExactMatchType: true,
|
||||
PrefixMatchType: true,
|
||||
SuffixMatchType: true,
|
||||
ContainsMatchType: true,
|
||||
RegexMatchType: true,
|
||||
}
|
||||
)
|
||||
|
||||
type McpServer struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
PathMatchType string `json:"path_match_type,omitempty"`
|
||||
PathMatchValue string `json:"path_match_value,omitempty"`
|
||||
UpstreamType string `json:"upstream_type,omitempty"`
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite,omitempty"`
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix,omitempty"`
|
||||
}
|
||||
70
pkg/ingress/kube/mcpserver/provider.go
Normal file
70
pkg/ingress/kube/mcpserver/provider.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2025 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 mcpserver
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type McpServerProvider interface {
|
||||
GetMcpServers() []*McpServer
|
||||
}
|
||||
|
||||
type McpRouteProviderAware interface {
|
||||
RegisterMcpServerProvider(provider McpServerProvider)
|
||||
}
|
||||
|
||||
type McpServerCache struct {
|
||||
mcpServers []*McpServer
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *McpServerCache) GetMcpServers() []*McpServer {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return c.mcpServers
|
||||
}
|
||||
|
||||
// SetMcpServers sets the mcp servers and returns true if the cached list is changed
|
||||
func (c *McpServerCache) SetMcpServers(mcpServers []*McpServer) bool {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
sortedMcpServers := make([]*McpServer, 0, len(mcpServers))
|
||||
sortedMcpServers = append(sortedMcpServers, mcpServers...)
|
||||
// Sort the mcp servers by PathMatchValue in descending order
|
||||
slices.SortFunc(sortedMcpServers, func(a, b *McpServer) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
if len(c.mcpServers) == len(sortedMcpServers) {
|
||||
changed := false
|
||||
for i := range c.mcpServers {
|
||||
if !reflect.DeepEqual(c.mcpServers[i], sortedMcpServers[i]) {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
c.mcpServers = sortedMcpServers
|
||||
return true
|
||||
}
|
||||
654
pkg/ingress/kube/mcpserver/provider_test.go
Normal file
654
pkg/ingress/kube/mcpserver/provider_test.go
Normal file
@@ -0,0 +1,654 @@
|
||||
// Copyright (c) 2025 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 mcpserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestMcpServerCache_GetSet(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
skip bool
|
||||
init []*McpServer
|
||||
input []*McpServer
|
||||
expect []*McpServer
|
||||
changed bool
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
init: nil,
|
||||
input: nil,
|
||||
changed: false,
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
name: "nil to non-nil",
|
||||
init: nil,
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length increase)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length decrease)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length unchanged + name field changed)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3-1",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3-1",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length unchanged + non-name field changed)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar-2.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test4",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar-2.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test4",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (content unchanged + order unchanged)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (content unchanged + order changed)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
if tt.skip {
|
||||
continue
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider := &McpServerCache{}
|
||||
|
||||
if provider.GetMcpServers() != nil {
|
||||
t.Fatalf("GetMcpServers doesn't return nil before testing.")
|
||||
}
|
||||
|
||||
_ = provider.SetMcpServers(tt.init)
|
||||
|
||||
changed := provider.SetMcpServers(tt.input)
|
||||
if changed != tt.changed {
|
||||
t.Fatalf("actual changed %t != expect changed %t", changed, tt.changed)
|
||||
return
|
||||
}
|
||||
|
||||
actual := provider.GetMcpServers()
|
||||
|
||||
if len(actual) != len(tt.expect) {
|
||||
t.Fatalf("actual length %d != expect length %d", len(actual), len(tt.expect))
|
||||
}
|
||||
for i := range actual {
|
||||
if diff := cmp.Diff(tt.expect[i], actual[i]); diff != "" {
|
||||
t.Fatalf("TestMcpServerCache_GetSet() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
else \
|
||||
echo "Installing AMD64 toolchain" && \
|
||||
apt-get update && \
|
||||
apt-get install -y gcc binutils; \
|
||||
apt-get install -y gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu; \
|
||||
fi
|
||||
|
||||
WORKDIR /workspace
|
||||
@@ -30,7 +30,7 @@ RUN go mod tidy
|
||||
RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc AS=aarch64-linux-gnu-as go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
|
||||
else \
|
||||
go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
|
||||
CC=x86_64-linux-gnu-gcc AS=x86_64-linux-gnu-as go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
|
||||
fi
|
||||
|
||||
FROM scratch AS output
|
||||
|
||||
@@ -20,28 +20,49 @@ Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filte
|
||||
|
||||
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
|
||||
|
||||
## 插件注册
|
||||
|
||||
在开发新的 Golang Filter 时,需要在`main.go` 的 `init()` 函数中注册你的插件。注册时需要提供插件名称、Filter 工厂函数和配置解析器:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
|
||||
"your-plugin-name", // 插件名称
|
||||
yourFilterFactory, // Filter 工厂函数
|
||||
&yourConfigParser{}, // 配置解析器
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
|
||||
多个 Golang Filter 插件可以共同编译到一个 `golang-filter.so` 文件中,通过 `plugin_name` 来指定要使用的插件。配置示例如下:
|
||||
|
||||
```yaml
|
||||
http_filters:
|
||||
- name: envoy.filters.http.golang
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
library_id: your-plugin-name
|
||||
library_path: "./golang-filter.so" # 包含多个插件的共享库文件
|
||||
plugin_name: your-plugin-name # 指定要使用的插件名称,需要与 init() 函数中注册的插件名称保持一致
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
your_config_here: value
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 快速构建
|
||||
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
如果是 arm64 架构,请设置 `GOARCH=arm64`:
|
||||
|
||||
```bash
|
||||
make build GOARCH=arm64
|
||||
```
|
||||
你也可以直接在 Higress 项目的根目录下执行 `make build-gateway-local` 来构建 Higress Gateway 镜像,`golang-filter.so` 将会自动构建并复制到镜像中。
|
||||
|
||||
@@ -20,16 +20,32 @@ The Golang HTTP Filter allows developers to write custom Envoy Filters using the
|
||||
|
||||
Please refer to [Envoy Golang HTTP Filter Example](https://github.com/envoyproxy/examples/tree/main/golang-http) to learn how to develop and run a basic Golang Filter.
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
When developing a new Golang Filter, you need to register your plugin in the `init()` function of `main.go`. The registration requires a plugin name, Filter factory function, and configuration parser:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
|
||||
"your-plugin-name", // Plugin name
|
||||
yourFilterFactory, // Filter factory function
|
||||
&yourConfigParser{}, // Configuration parser
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
Multiple Golang Filter plugins can be compiled into a single `golang-filter.so` file, and the desired plugin can be specified using `plugin_name`. Here's an example configuration:
|
||||
|
||||
```yaml
|
||||
http_filters:
|
||||
- name: envoy.filters.http.golang
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./my-go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
library_id: your-plugin-name
|
||||
library_path: "./golang-filter.so" # Shared library file containing multiple plugins
|
||||
plugin_name: your-plugin-name # Specify which plugin to use, must match the name registered in init()
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
@@ -41,5 +57,13 @@ http_filters:
|
||||
Use the following command to quickly build the golang filter plugin:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
```
|
||||
make build
|
||||
```
|
||||
|
||||
If you are on an arm64 architecture, please set `GOARCH=arm64`:
|
||||
|
||||
```bash
|
||||
make build GOARCH=arm64
|
||||
```
|
||||
|
||||
Alternatively, you can build the Higress Gateway image directly by running `make build-gateway-local` in the root directory of the Higress project. The `golang-filter.so` file will be automatically built and included in the image.
|
||||
|
||||
@@ -2,7 +2,7 @@ module github.com/alibaba/higress/plugins/golang-filter
|
||||
|
||||
go 1.22
|
||||
|
||||
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644
|
||||
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c
|
||||
|
||||
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
|
||||
|
||||
|
||||
@@ -234,8 +234,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644 h1:wiLDdiOT3BcTQSFs8oTMu54GIiPFSwKLuWo5J0Cd9b8=
|
||||
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
|
||||
@@ -3,27 +3,22 @@
|
||||
|
||||
## 概述
|
||||
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,用于实现服务器端事件(SSE)和消息通信功能。该插件支持多种数据库类型,并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,提供了统一的 MCP (Model Context Protocol) 服务接口。它支持多种后端服务的集成,包括:
|
||||
|
||||
> **注意**:MCP Server需要 Higress 2.1.0 或更高版本才能使用。
|
||||
## 项目结构
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # 配置解析相关代码
|
||||
├── filter.go # 请求处理相关代码
|
||||
├── internal/ # 内部实现逻辑
|
||||
├── servers/ # MCP 服务器实现
|
||||
├── go.mod # Go模块依赖定义
|
||||
└── go.sum # Go模块依赖校验
|
||||
```
|
||||
## MCP Server开发指南
|
||||
- 数据库服务:通过 GORM 支持多种数据库的访问和管理
|
||||
- 配置中心:支持 Nacos 配置中心的集成
|
||||
- 可扩展性:支持自定义服务器实现,方便集成其他服务
|
||||
|
||||
> **注意**:MCP Server 需要 Higress 2.1.0 或更高版本才能使用。
|
||||
|
||||
## MCP Server 开发指南
|
||||
|
||||
```go
|
||||
// 在init函数中注册你的服务器
|
||||
// 参数1: 服务器名称
|
||||
// 参数2: 配置结构体实例
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// 服务器配置结构体
|
||||
@@ -43,8 +38,8 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
// 创建新的MCP服务器实例
|
||||
// serverName: 服务器名称
|
||||
// 返回值: MCP服务器实例和可能的错误
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(serverName, Version)
|
||||
|
||||
// 添加工具方法到服务器
|
||||
// mcpServer.AddTool()
|
||||
60
plugins/golang-filter/mcp-server/README_en.md
Normal file
60
plugins/golang-filter/mcp-server/README_en.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# MCP Server
|
||||
English | [简体中文](./README.md)
|
||||
|
||||
## Overview
|
||||
|
||||
MCP Server is a Golang Filter plugin based on Envoy that provides a unified MCP (Model Context Protocol) service interface. It supports integration with various backend services, including:
|
||||
|
||||
- Database Services: Supports multiple database access and management through GORM
|
||||
- Configuration Service: Supports integration with Nacos configuration service
|
||||
- Extensibility: Supports custom server implementations for easy integration with other services
|
||||
|
||||
> **Note**: MCP Server requires Higress version 2.1.0 or higher to be used.
|
||||
|
||||
## MCP Server Development Guide
|
||||
|
||||
```go
|
||||
// Register your server in the init function
|
||||
// Parameter 1: Server name
|
||||
// Parameter 2: Configuration struct instance
|
||||
func init() {
|
||||
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// Server configuration struct
|
||||
type DemoConfig struct {
|
||||
helloworld string
|
||||
}
|
||||
|
||||
// Parse configuration method
|
||||
// Parse and validate configuration items from the config map
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
helloworld, ok := config["helloworld"].(string)
|
||||
if !ok { return errors.New("missing helloworld")}
|
||||
c.helloworld = helloworld
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new MCP server instance
|
||||
// serverName: Server name
|
||||
// Returns: MCP server instance and possible error
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(serverName, Version)
|
||||
|
||||
// Add tool methods to the server
|
||||
// mcpServer.AddTool()
|
||||
|
||||
// Add resources to the server
|
||||
// mcpServer.AddResource()
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**:
|
||||
You need to use underscore imports in config.go to execute the package's init function
|
||||
```go
|
||||
import (
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
)
|
||||
```
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
@@ -99,7 +100,7 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
serverInstance, err := server.NewServer(serverName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
|
||||
return nil, fmt.Errorf("failed to initialize MCP Server: %w", err)
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, &SSEServerWrapper{
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/vo"
|
||||
)
|
||||
|
||||
type NacosMcpRegsitry struct {
|
||||
type NacosMcpRegistry struct {
|
||||
serviceMatcher map[string]string
|
||||
configClient config_client.IConfigClient
|
||||
namingClient naming_client.INamingClient
|
||||
@@ -27,7 +27,7 @@ type NacosMcpRegsitry struct {
|
||||
const DEFAULT_SERVICE_LIST_MAX_PGSIZXE = 10000
|
||||
const MCP_TOOL_SUBFIX = "-mcp-tools.json"
|
||||
|
||||
func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
|
||||
func (n *NacosMcpRegistry) ListToolsDescription() []*registry.ToolDescription {
|
||||
if n.toolsDescription == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
|
||||
func (n *NacosMcpRegistry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
|
||||
if n.toolsRpcContext == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
@@ -47,11 +47,11 @@ func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcCont
|
||||
return tool, ok
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
|
||||
func (n *NacosMcpRegistry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
|
||||
n.toolChangeEventListeners = append(n.toolChangeEventListeners, listener)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsList() bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsList() bool {
|
||||
changed := false
|
||||
for group, serviceMatcher := range n.serviceMatcher {
|
||||
if n.refreshToolsListForGroup(group, serviceMatcher) {
|
||||
@@ -61,7 +61,7 @@ func (n *NacosMcpRegsitry) refreshToolsList() bool {
|
||||
return changed
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
|
||||
services, err := n.namingClient.GetAllServicesInfo(vo.GetAllServiceInfoParam{
|
||||
GroupName: group,
|
||||
PageNo: 1,
|
||||
@@ -134,7 +134,7 @@ func getFormatServiceName(group string, service string) string {
|
||||
return fmt.Sprintf("%s_%s", group, service)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
|
||||
func (n *NacosMcpRegistry) deleteToolForService(group string, service string) {
|
||||
toolsNeedReset := []string{}
|
||||
|
||||
formatServiceName := getFormatServiceName(group, service)
|
||||
@@ -150,7 +150,7 @@ func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
|
||||
|
||||
if newConfig == nil {
|
||||
dataId := makeToolsConfigId(service)
|
||||
@@ -243,7 +243,7 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.CredentialInfo {
|
||||
func (n *NacosMcpRegistry) GetCredential(name string, group string) *registry.CredentialInfo {
|
||||
dataId := makeCredentialDataId(name)
|
||||
content, err := n.configClient.GetConfig(vo.ConfigParam{
|
||||
DataId: dataId,
|
||||
@@ -265,11 +265,11 @@ func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.Cr
|
||||
return &credential
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsListForService(group string, service string) bool {
|
||||
return n.refreshToolsListForServiceWithContent(group, service, nil, nil)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) listenToService(group string, service string) {
|
||||
func (n *NacosMcpRegistry) listenToService(group string, service string) {
|
||||
|
||||
// config changed, tools description may be changed
|
||||
err := n.configClient.ListenConfig(vo.ConfigParam{
|
||||
|
||||
@@ -35,7 +35,7 @@ func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegis
|
||||
resetToolsToMcpServer(l.mcpServer, reg)
|
||||
}
|
||||
|
||||
func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
|
||||
func CreateNacosMcpRegistry(config *NacosConfig) (*NacosMcpRegistry, error) {
|
||||
sc := []constant.ServerConfig{
|
||||
*constant.NewServerConfig(*config.ServerAddr, 8848, constant.WithContextPath("/nacos")),
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
|
||||
return nil, fmt.Errorf("failed to initial naming config client: %w", err)
|
||||
}
|
||||
|
||||
return &NacosMcpRegsitry{
|
||||
return &NacosMcpRegistry{
|
||||
configClient: configClient,
|
||||
namingClient: namingClient,
|
||||
serviceMatcher: *config.ServiceMatcher,
|
||||
@@ -143,7 +143,7 @@ func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
"1.0.0",
|
||||
)
|
||||
|
||||
nacosRegistry, err := CreateNacosMcpRegsitry(c)
|
||||
nacosRegistry, err := CreateNacosMcpRegistry(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize NacosMcpRegistry: %w", err)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
|
||||
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []common.ServerTool{}
|
||||
tools := reg.ListToolsDesciption()
|
||||
tools := reg.ListToolsDescription()
|
||||
for _, tool := range tools {
|
||||
wrappedTools = append(wrappedTools, common.ServerTool{
|
||||
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
|
||||
|
||||
@@ -36,7 +36,7 @@ type ToolChangeEventListener interface {
|
||||
}
|
||||
|
||||
type McpServerRegistry interface {
|
||||
ListToolsDesciption() []*ToolDescription
|
||||
ListToolsDescription() []*ToolDescription
|
||||
GetToolRpcContext(toolname string) (*RpcContext, bool)
|
||||
RegisterToolChangeEventListener(listener ToolChangeEventListener)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func selectOneInstance(ctx *RpcContext) (*Instance, error) {
|
||||
return &select_instance, nil
|
||||
}
|
||||
|
||||
func getRemoteCallhandle(ctx *RpcContext) (RemoteCallHandle, error) {
|
||||
func getRemoteCallHandle(ctx *RpcContext) (RemoteCallHandle, error) {
|
||||
if ctx.Protocol == PROTOCOL_HTTP || ctx.Protocol == PROTOCOL_HTTPS {
|
||||
return newHttpRemoteCallHandle(ctx)
|
||||
} else {
|
||||
@@ -192,7 +192,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
|
||||
return nil, fmt.Errorf("Unknown tool %s", toolName)
|
||||
}
|
||||
|
||||
remoteHandle, err := getRemoteCallhandle(ctx)
|
||||
remoteHandle, err := getRemoteCallHandle(ctx)
|
||||
if remoteHandle == nil {
|
||||
return nil, fmt.Errorf("Unknown backend protocol %s", ctx.Protocol)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package gorm
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,14 @@ type DBClient struct {
|
||||
panicCount int32 // Add panic counter
|
||||
}
|
||||
|
||||
// supports database types
|
||||
const (
|
||||
MYSQL = "mysql"
|
||||
POSTGRES = "postgres"
|
||||
CLICKHOUSE = "clickhouse"
|
||||
SQLITE = "sqlite"
|
||||
)
|
||||
|
||||
// NewDBClient creates a new DBClient instance and establishes a connection to the database
|
||||
func NewDBClient(dsn string, dbType string, stop chan struct{}) *DBClient {
|
||||
client := &DBClient{
|
||||
@@ -53,13 +62,13 @@ func (c *DBClient) connect() error {
|
||||
}
|
||||
|
||||
switch c.dbType {
|
||||
case "postgres":
|
||||
case POSTGRES:
|
||||
db, err = gorm.Open(postgres.Open(c.dsn), &gormConfig)
|
||||
case "clickhouse":
|
||||
case CLICKHOUSE:
|
||||
db, err = gorm.Open(clickhouse.Open(c.dsn), &gormConfig)
|
||||
case "mysql":
|
||||
case MYSQL:
|
||||
db, err = gorm.Open(mysql.Open(c.dsn), &gormConfig)
|
||||
case "sqlite":
|
||||
case SQLITE:
|
||||
db, err = gorm.Open(sqlite.Open(c.dsn), &gormConfig)
|
||||
default:
|
||||
return fmt.Errorf("unsupported database type %s", c.dbType)
|
||||
@@ -125,25 +134,164 @@ func (c *DBClient) reconnectLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteSQL executes a raw SQL query and returns the result as a slice of maps
|
||||
func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]interface{}, error) {
|
||||
func (c *DBClient) reconnectIfDbEmpty() error {
|
||||
if c.db == nil {
|
||||
// Trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("database is not connected, attempting to reconnect")
|
||||
return fmt.Errorf("database is not connected, attempting to reconnect")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(query, args...).Rows()
|
||||
func (c *DBClient) handleSQLError(err error) error {
|
||||
if err != nil {
|
||||
// If execution fails, connection might be lost, trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
return fmt.Errorf("failed to execute SQL: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DescribeTable Get the structure of a specific table.
|
||||
func (c *DBClient) DescribeTable(table string) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
switch c.dbType {
|
||||
case MYSQL:
|
||||
sql = `
|
||||
select
|
||||
column_name,
|
||||
column_type,
|
||||
is_nullable,
|
||||
column_key,
|
||||
column_default,
|
||||
extra,
|
||||
column_comment
|
||||
from information_schema.columns
|
||||
where table_schema = database() and table_name = ?
|
||||
`
|
||||
return c.Query(sql, table)
|
||||
|
||||
case POSTGRES:
|
||||
sql = `
|
||||
select
|
||||
column_name,
|
||||
data_type as column_type,
|
||||
is_nullable,
|
||||
case
|
||||
when column_default like 'nextval%' then 'auto_increment'
|
||||
when column_default is not null then 'default'
|
||||
else ''
|
||||
end as column_key,
|
||||
column_default,
|
||||
case
|
||||
when column_default like 'nextval%' then 'auto_increment'
|
||||
else ''
|
||||
end as extra,
|
||||
col_description((select oid from pg_class where relname = ?), ordinal_position) as column_comment
|
||||
from information_schema.columns
|
||||
where table_name = ?
|
||||
`
|
||||
lowerTable := strings.ToLower(table)
|
||||
return c.Query(sql, lowerTable, lowerTable)
|
||||
|
||||
case CLICKHOUSE:
|
||||
sql = `
|
||||
select
|
||||
name as column_name,
|
||||
type as column_type,
|
||||
if(is_nullable, 'YES', 'NO') as is_nullable,
|
||||
default_kind as column_key,
|
||||
default_expression as column_default,
|
||||
default_kind as extra,
|
||||
comment as column_comment
|
||||
from system.columns
|
||||
where database = currentDatabase() and table = ?
|
||||
`
|
||||
return c.Query(sql, table)
|
||||
|
||||
case SQLITE:
|
||||
sql = `
|
||||
select
|
||||
name as column_name,
|
||||
type as column_type,
|
||||
not (notnull = 1) as is_nullable,
|
||||
pk as column_key,
|
||||
dflt_value as column_default,
|
||||
'' as extra,
|
||||
'' as column_comment
|
||||
from pragma_table_info(?)
|
||||
`
|
||||
return c.Query(sql, table)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", c.dbType)
|
||||
}
|
||||
}
|
||||
|
||||
// ListTables List all tables in the connected database.
|
||||
func (c *DBClient) ListTables() ([]string, error) {
|
||||
var sql string
|
||||
switch c.dbType {
|
||||
case MYSQL:
|
||||
sql = "show tables"
|
||||
case POSTGRES:
|
||||
sql = "select tablename from pg_tables where schemaname = 'public'"
|
||||
case CLICKHOUSE:
|
||||
sql = "select name from system.tables where database = currentDatabase()"
|
||||
case SQLITE:
|
||||
sql = "select name from sqlite_master where type='table'"
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", c.dbType)
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(sql).Rows()
|
||||
if err := c.handleSQLError(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var table string
|
||||
if err := rows.Scan(&table); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan table name: %w", err)
|
||||
}
|
||||
tables = append(tables, table)
|
||||
}
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// Execute executes an INSERT, UPDATE, or DELETE raw SQL and returns the rows affected
|
||||
func (c *DBClient) Execute(sql string, args ...interface{}) (int64, error) {
|
||||
if err := c.reconnectIfDbEmpty(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
tx := c.db.Exec(sql, args...)
|
||||
if err := c.handleSQLError(tx.Error); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer tx.Commit()
|
||||
|
||||
return tx.RowsAffected, nil
|
||||
}
|
||||
|
||||
// Query executes a raw SQL query and returns the result as a slice of maps
|
||||
func (c *DBClient) Query(sql string, args ...interface{}) ([]map[string]interface{}, error) {
|
||||
if err := c.reconnectIfDbEmpty(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(sql, args...).Rows()
|
||||
if err := c.handleSQLError(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
@@ -49,11 +49,24 @@ func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
)
|
||||
|
||||
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
|
||||
descriptionSuffix := fmt.Sprintf("in database %s. Database description: %s", c.dbType, c.description)
|
||||
// Add query tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s. Database description: %s", c.dbType, c.description), GetQueryToolSchema()),
|
||||
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query %s", descriptionSuffix), GetQueryToolSchema()),
|
||||
HandleQueryTool(dbClient),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("execute", fmt.Sprintf("Execute an insert, update, or delete SQL %s", descriptionSuffix), GetExecuteToolSchema()),
|
||||
HandleExecuteTool(dbClient),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list tables", fmt.Sprintf("List all tables %s", descriptionSuffix), GetListTablesToolSchema()),
|
||||
HandleListTablesTool(dbClient),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("describe table", fmt.Sprintf("Get the structure of a specific table %s", descriptionSuffix), GetDescribeTableToolSchema()),
|
||||
HandleDescribeTableTool(dbClient),
|
||||
)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
|
||||
@@ -18,27 +18,80 @@ func HandleQueryTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("invalid message argument")
|
||||
}
|
||||
|
||||
results, err := dbClient.ExecuteSQL(message)
|
||||
results, err := dbClient.Query(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal SQL results: %w", err)
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleExecuteTool handles SQL INSERT, UPDATE, or DELETE execution
|
||||
func HandleExecuteTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
message, ok := arguments["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid message argument")
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(jsonData),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
results, err := dbClient.Execute(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleListTablesTool handles list all tables
|
||||
func HandleListTablesTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
results, err := dbClient.ListTables()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDescribeTableTool handles describe table
|
||||
func HandleDescribeTableTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
message, ok := arguments["table"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid message argument")
|
||||
}
|
||||
|
||||
results, err := dbClient.DescribeTable(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// buildCallToolResult builds the call tool result
|
||||
func buildCallToolResult(results any) (*mcp.CallToolResult, error) {
|
||||
jsonData, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal SQL results: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(jsonData),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetQueryToolSchema returns the schema for query tool
|
||||
func GetQueryToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
@@ -53,3 +106,44 @@ func GetQueryToolSchema() json.RawMessage {
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// GetExecuteToolSchema returns the schema for execute tool
|
||||
func GetExecuteToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The sql to execute"
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// GetDescribeTableToolSchema returns the schema for DescribeTable tool
|
||||
func GetDescribeTableToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {
|
||||
"type": "string",
|
||||
"description": "table name"
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// GetListTablesToolSchema returns the schema for ListTables tool
|
||||
func GetListTablesToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
95
plugins/golang-filter/mcp-server/servers/higress/client.go
Normal file
95
plugins/golang-filter/mcp-server/servers/higress/client.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package higress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// HigressClient handles Higress Console API connections and operations
|
||||
type HigressClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
client := &HigressClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
password: password,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
api.LogInfof("Higress Console client initialized: %s", baseURL)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HigressClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("POST", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("PUT", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Delete(path string) ([]byte, error) {
|
||||
return c.request("DELETE", path, nil)
|
||||
}
|
||||
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request data: %w", err)
|
||||
}
|
||||
body = bytes.NewBuffer(jsonData)
|
||||
api.LogDebugf("Higress API %s %s: %s", method, url, string(jsonData))
|
||||
} else {
|
||||
api.LogDebugf("Higress API %s %s", method, url)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源和插件等资源。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 路由管理
|
||||
- `list-routes`: 列出路由
|
||||
- `get-route`: 获取路由
|
||||
- `add-route`: 添加路由
|
||||
- `update-route`: 更新路由
|
||||
|
||||
### 服务来源管理
|
||||
- `list-service-sources`: 列出服务来源
|
||||
- `get-service-source`: 获取服务来源
|
||||
- `add-service-source`: 添加服务来源
|
||||
- `update-service-source`: 更新服务来源
|
||||
|
||||
### 插件管理
|
||||
- `get-plugin`: 获取插件配置
|
||||
- `delete-plugin`: 删除插件
|
||||
- `update-request-block-plugin`: 更新 request-block 插件配置
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `higressURL` | string | 必填 | Higress Console 的 URL 地址 |
|
||||
| `username` | string | 必填 | Higress Console 登录用户名 |
|
||||
| `password` | string | 必填 | Higress Console 登录密码 |
|
||||
| `description` | string | 可选 | 服务器描述信息 |
|
||||
|
||||
配置示例:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: higress
|
||||
meta.helm.sh/release-namespace: higress-system
|
||||
labels:
|
||||
app: higress-gateway
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
app.kubernetes.io/name: higress-gateway
|
||||
app.kubernetes.io/version: 2.1.4
|
||||
helm.sh/chart: higress-core-2.1.4
|
||||
higress: higress-system-higress-gateway
|
||||
name: higress-config
|
||||
namespace: higress-system
|
||||
data:
|
||||
higress: |-
|
||||
mcpServer:
|
||||
sse_path_suffix: /sse # SSE 连接的路径后缀
|
||||
enable: true # 启用 MCP Server
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis服务地址
|
||||
username: "" # Redis用户名(可选)
|
||||
password: "" # Redis密码(可选)
|
||||
db: 0 # Redis数据库(可选)
|
||||
match_list: # MCP Server 会话保持路由规则(当匹配下面路径时,将被识别为一个 MCP 会话,通过 SSE 等机制进行会话保持)
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-api
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-api-mcp-server # MCP Server 名称
|
||||
path: /higress-api # 访问路径,需要与 match_list 中的配置匹配
|
||||
type: higress-api # 类型和 RegisterServer 一致
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server provides MCP tools to manage Higress routes, service sources, plugins and other resources.
|
||||
|
||||
## Features
|
||||
|
||||
### Route Management
|
||||
- `list-routes`: List routes
|
||||
- `get-route`: Get route
|
||||
- `add-route`: Add route
|
||||
- `update-route`: Update route
|
||||
|
||||
### Service Source Management
|
||||
- `list-service-sources`: List service sources
|
||||
- `get-service-source`: Get service source
|
||||
- `add-service-source`: Add service source
|
||||
- `update-service-source`: Update service source
|
||||
|
||||
### Plugin Management
|
||||
- `get-plugin`: Get plugin configuration
|
||||
- `delete-plugin`: Delete plugin
|
||||
- `update-request-block-plugin`: Update request block configuration
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `higressURL` | string | Required | Higress Console URL address |
|
||||
| `username` | string | Required | Higress Console login username |
|
||||
| `password` | string | Required | Higress Console login password |
|
||||
| `description` | string | Optional | MCP Server description |
|
||||
|
||||
Configuration Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: higress
|
||||
meta.helm.sh/release-namespace: higress-system
|
||||
labels:
|
||||
app: higress-gateway
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
app.kubernetes.io/name: higress-gateway
|
||||
app.kubernetes.io/version: 2.1.4
|
||||
helm.sh/chart: higress-core-2.1.4
|
||||
higress: higress-system-higress-gateway
|
||||
name: higress-config
|
||||
namespace: higress-system
|
||||
data:
|
||||
higress: |-
|
||||
mcpServer:
|
||||
sse_path_suffix: /sse # SSE connection path suffix
|
||||
enable: true # Enable MCP Server
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis service address
|
||||
username: "" # Redis username (optional)
|
||||
password: "" # Redis password (optional)
|
||||
db: 0 # Redis database (optional)
|
||||
match_list: # MCP Server session persistence routing rules (when matching the following paths, it will be recognized as an MCP session and maintained through SSE)
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-api
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-api-mcp-server # MCP Server name
|
||||
path: /higress-api # Access path, needs to match the configuration in match_list
|
||||
type: higress-api # Type defined in RegisterServer function
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
package higress_ops
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
|
||||
func init() {
|
||||
common.GlobalRegistry.RegisterServer("higress-api", &HigressConfig{})
|
||||
}
|
||||
|
||||
type HigressConfig struct {
|
||||
higressURL string
|
||||
username string
|
||||
password string
|
||||
description string
|
||||
}
|
||||
|
||||
func (c *HigressConfig) ParseConfig(config map[string]interface{}) error {
|
||||
higressURL, ok := config["higressURL"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing higressURL")
|
||||
}
|
||||
c.higressURL = higressURL
|
||||
|
||||
username, ok := config["username"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing username")
|
||||
}
|
||||
c.username = username
|
||||
|
||||
password, ok := config["password"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing password")
|
||||
}
|
||||
c.password = password
|
||||
|
||||
if desc, ok := config["description"].(string); ok {
|
||||
c.description = desc
|
||||
} else {
|
||||
c.description = "Higress API MCP Server, which invokes Higress Console APIs to manage resources such as routes, services, and plugins."
|
||||
}
|
||||
|
||||
api.LogDebugf("HigressConfig ParseConfig: higressURL=%s, username=%s, description=%s",
|
||||
c.higressURL, c.username, c.description)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HigressConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
common.WithInstructions("This is a Higress API MCP Server"),
|
||||
)
|
||||
|
||||
// Initialize Higress API client
|
||||
client := higress.NewHigressClient(c.higressURL, c.username, c.password)
|
||||
|
||||
// Register all tools
|
||||
tools.RegisterRouteTools(mcpServer, client)
|
||||
tools.RegisterServiceTools(mcpServer, client)
|
||||
plugins.RegisterCommonPluginTools(mcpServer, client)
|
||||
plugins.RegisterRequestBlockPluginTools(mcpServer, client)
|
||||
|
||||
api.LogInfof("Higress MCP Server initialized: %s", serverName)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// RegisterCommonPluginTools registers all common plugin management tools
|
||||
func RegisterCommonPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Get plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-plugin", "Get configuration for a specific plugin", getPluginConfigSchema()),
|
||||
handleGetPluginConfig(client),
|
||||
)
|
||||
|
||||
// Delete plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-plugin", "Delete configuration for a specific plugin", getPluginConfigSchema()),
|
||||
handleDeletePluginConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
pluginName, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
scope, ok := arguments["scope"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'scope' argument")
|
||||
}
|
||||
|
||||
if !IsValidScope(scope) {
|
||||
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
|
||||
}
|
||||
|
||||
// Parse resource_name (required for non-global scopes)
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Get(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
pluginName, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
scope, ok := arguments["scope"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'scope' argument")
|
||||
}
|
||||
|
||||
if !IsValidScope(scope) {
|
||||
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
|
||||
}
|
||||
|
||||
// Parse resource_name (required for non-global scopes)
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Delete(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the plugin"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
|
||||
"description": "The scope at which the plugin is applied"
|
||||
},
|
||||
"resource_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "scope"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const RequestBlockPluginName = "request-block"
|
||||
|
||||
// RequestBlockConfig represents the configuration for request-block plugin
|
||||
type RequestBlockConfig struct {
|
||||
BlockBodies []string `json:"block_bodies,omitempty"`
|
||||
BlockHeaders []string `json:"block_headers,omitempty"`
|
||||
BlockUrls []string `json:"block_urls,omitempty"`
|
||||
BlockedCode int `json:"blocked_code,omitempty"`
|
||||
CaseSensitive bool `json:"case_sensitive,omitempty"`
|
||||
}
|
||||
|
||||
// RequestBlockInstance represents a request-block plugin instance
|
||||
type RequestBlockInstance = PluginInstance[RequestBlockConfig]
|
||||
|
||||
// RequestBlockResponse represents the API response for request-block plugin
|
||||
type RequestBlockResponse = higress.APIResponse[RequestBlockInstance]
|
||||
|
||||
// RegisterRequestBlockPluginTools registers all request block plugin management tools
|
||||
func RegisterRequestBlockPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Update request block configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(fmt.Sprintf("update-%s-plugin", RequestBlockPluginName), "Update request block plugin configuration", getAddOrUpdateRequestBlockConfigSchema()),
|
||||
handleAddOrUpdateRequestBlockConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
scope, ok := arguments["scope"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'scope' argument")
|
||||
}
|
||||
|
||||
if !IsValidScope(scope) {
|
||||
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
|
||||
}
|
||||
|
||||
enabled, ok := arguments["enabled"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'enabled' argument")
|
||||
}
|
||||
|
||||
configurations, ok := arguments["configurations"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'configurations' argument")
|
||||
}
|
||||
|
||||
// Parse resource_name for non-global scopes
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
// Validate and get resource_name
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path
|
||||
path := BuildPluginPath(RequestBlockPluginName, scope, resourceName)
|
||||
|
||||
// Get current request block configuration to merge with updates
|
||||
currentBody, err := client.Get(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current request block configuration: %w", err)
|
||||
}
|
||||
|
||||
var response RequestBlockResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current request block response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
currentConfig.Enabled = enabled
|
||||
currentConfig.Scope = scope
|
||||
|
||||
// Convert the input configurations to RequestBlockConfig and merge
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig RequestBlockConfig
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse request block configurations: %w", err)
|
||||
}
|
||||
|
||||
// Update configurations (overwrite with new values where provided)
|
||||
if newConfig.BlockBodies != nil {
|
||||
currentConfig.Configurations.BlockBodies = newConfig.BlockBodies
|
||||
}
|
||||
if newConfig.BlockHeaders != nil {
|
||||
currentConfig.Configurations.BlockHeaders = newConfig.BlockHeaders
|
||||
}
|
||||
if newConfig.BlockUrls != nil {
|
||||
currentConfig.Configurations.BlockUrls = newConfig.BlockUrls
|
||||
}
|
||||
if newConfig.BlockedCode != 0 {
|
||||
currentConfig.Configurations.BlockedCode = newConfig.BlockedCode
|
||||
}
|
||||
currentConfig.Configurations.CaseSensitive = newConfig.CaseSensitive
|
||||
|
||||
respBody, err := client.Put(path, currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update request block config at scope '%s': %w", scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAddOrUpdateRequestBlockConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
|
||||
"description": "The scope at which the plugin is applied"
|
||||
},
|
||||
|
||||
"resource_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the plugin is enabled"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block_bodies": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request body content"
|
||||
},
|
||||
"block_headers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request headers"
|
||||
},
|
||||
"block_urls": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request URLs"
|
||||
},
|
||||
"blocked_code": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 599,
|
||||
"description": "HTTP status code to return when a block is matched"
|
||||
},
|
||||
"case_sensitive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the block matching is case sensitive"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["scope", "enabled", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package plugins
|
||||
|
||||
// PluginTargets represents the targets for different scopes
|
||||
type PluginTargets struct {
|
||||
Domain string `json:"DOMAIN,omitempty"`
|
||||
Service string `json:"SERVICE,omitempty"`
|
||||
Route string `json:"ROUTE,omitempty"`
|
||||
}
|
||||
|
||||
// PluginInstance represents a plugin instance configuration
|
||||
type PluginInstance[T any] struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Targets PluginTargets `json:"targets,omitempty"`
|
||||
PluginName string `json:"pluginName,omitempty"`
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
Internal bool `json:"internal,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RawConfigurations string `json:"rawConfigurations,omitempty"`
|
||||
Configurations T `json:"configurations,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package plugins
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
ScopeGlobal = "GLOBAL"
|
||||
ScopeDomain = "DOMAIN"
|
||||
ScopeService = "SERVICE"
|
||||
ScopeRoute = "ROUTE"
|
||||
)
|
||||
|
||||
// ValidScopes contains all valid plugin scopes
|
||||
var ValidScopes = []string{ScopeGlobal, ScopeDomain, ScopeService, ScopeRoute}
|
||||
|
||||
// IsValidScope checks if the given scope is valid
|
||||
func IsValidScope(scope string) bool {
|
||||
for _, validScope := range ValidScopes {
|
||||
if scope == validScope {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildPluginPath builds the API path for plugin operations based on scope and resource
|
||||
func BuildPluginPath(pluginName, scope, resourceName string) string {
|
||||
switch scope {
|
||||
case ScopeGlobal:
|
||||
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
|
||||
case ScopeDomain:
|
||||
return fmt.Sprintf("/v1/domains/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
case ScopeService:
|
||||
return fmt.Sprintf("/v1/services/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
case ScopeRoute:
|
||||
return fmt.Sprintf("/v1/routes/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
default:
|
||||
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// Route represents a route configuration
|
||||
type Route struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Path *RoutePath `json:"path,omitempty"`
|
||||
Methods []string `json:"methods,omitempty"`
|
||||
Headers []RouteMatch `json:"headers,omitempty"`
|
||||
URLParams []RouteMatch `json:"urlParams,omitempty"`
|
||||
Services []RouteService `json:"services,omitempty"`
|
||||
AuthConfig *RouteAuthConfig `json:"authConfig,omitempty"`
|
||||
CustomConfigs map[string]interface{} `json:"customConfigs,omitempty"`
|
||||
}
|
||||
|
||||
// RoutePath represents path matching configuration
|
||||
type RoutePath struct {
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
// RouteMatch represents header or URL parameter matching configuration
|
||||
type RouteMatch struct {
|
||||
Key string `json:"key"`
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
}
|
||||
|
||||
// RouteService represents a service in the route
|
||||
type RouteService struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
// RouteAuthConfig represents authentication configuration for a route
|
||||
type RouteAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedConsumers []string `json:"allowedConsumers,omitempty"`
|
||||
}
|
||||
|
||||
// RouteResponse represents the API response for route operations
|
||||
type RouteResponse = higress.APIResponse[Route]
|
||||
|
||||
// RegisterRouteTools registers all route management tools
|
||||
func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all routes
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-routes", mcp.WithDescription("List all available routes")),
|
||||
handleListRoutes(client),
|
||||
)
|
||||
|
||||
// Get specific route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-route", "Get detailed information about a specific route", getRouteSchema()),
|
||||
handleGetRoute(client),
|
||||
)
|
||||
|
||||
// Add new route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-route", "Add a new route", getAddRouteSchema()),
|
||||
handleAddRoute(client),
|
||||
)
|
||||
|
||||
// Update existing route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-route", "Update an existing route", getUpdateRouteSchema()),
|
||||
handleUpdateRoute(client),
|
||||
)
|
||||
|
||||
// Delete existing route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-route", "Delete an existing route", getRouteSchema()),
|
||||
handleDeleteRoute(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListRoutes(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/routes")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list routes: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in configurations")
|
||||
}
|
||||
if _, ok := configurations["path"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'path' in configurations")
|
||||
}
|
||||
if _, ok := configurations["services"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'services' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/routes", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Get current route configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current route configuration: %w", err)
|
||||
}
|
||||
|
||||
var response RouteResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current route response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
|
||||
// Update configurations using JSON marshal/unmarshal for type conversion
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig Route
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse route configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Domains != nil {
|
||||
currentConfig.Domains = newConfig.Domains
|
||||
}
|
||||
if newConfig.Path != nil {
|
||||
currentConfig.Path = newConfig.Path
|
||||
}
|
||||
if newConfig.Methods != nil {
|
||||
currentConfig.Methods = newConfig.Methods
|
||||
}
|
||||
if newConfig.Headers != nil {
|
||||
currentConfig.Headers = newConfig.Headers
|
||||
}
|
||||
if newConfig.URLParams != nil {
|
||||
currentConfig.URLParams = newConfig.URLParams
|
||||
}
|
||||
if newConfig.Services != nil {
|
||||
currentConfig.Services = newConfig.Services
|
||||
}
|
||||
if newConfig.AuthConfig != nil {
|
||||
currentConfig.AuthConfig = newConfig.AuthConfig
|
||||
}
|
||||
if newConfig.CustomConfigs != nil {
|
||||
currentConfig.CustomConfigs = newConfig.CustomConfigs
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/routes/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed"
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "List of path match conditions"
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
|
||||
"description": "List of HTTP methods"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Header key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of header match conditions"
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Parameter key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of URL parameter match conditions"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Service name"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "List of services for this route"
|
||||
},
|
||||
"customConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Dictionary of custom configurations"
|
||||
}
|
||||
},
|
||||
"required": ["name", "path", "services"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getUpdateRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed",
|
||||
"maxItems": 1
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "The path configuration"
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
|
||||
"description": "List of HTTP methods"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Header key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of header match conditions"
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Parameter key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of URL parameter match conditions"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Service name"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "List of services for this route"
|
||||
},
|
||||
"customConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Dictionary of custom configurations"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// ServiceSource represents a service source configuration
|
||||
type ServiceSource struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Domain string `json:"domain"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
SNI *string `json:"sni,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
AuthN *ServiceSourceAuthN `json:"authN,omitempty"`
|
||||
Valid bool `json:"valid,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSourceAuthN represents authentication configuration for service source
|
||||
type ServiceSourceAuthN struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSourceResponse represents the API response for service source operations
|
||||
type ServiceSourceResponse = higress.APIResponse[ServiceSource]
|
||||
|
||||
// RegisterServiceTools registers all service source management tools
|
||||
func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all service sources
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-service-sources", mcp.WithDescription("List all available service sources")),
|
||||
handleListServiceSources(client),
|
||||
)
|
||||
|
||||
// Get specific service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-service-source", "Get detailed information about a specific service source", getServiceSourceSchema()),
|
||||
handleGetServiceSource(client),
|
||||
)
|
||||
|
||||
// Add new service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-service-source", "Add a new service source", getAddServiceSourceSchema()),
|
||||
handleAddServiceSource(client),
|
||||
)
|
||||
|
||||
// Update existing service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-service-source", "Update an existing service source", getUpdateServiceSourceSchema()),
|
||||
handleUpdateServiceSource(client),
|
||||
)
|
||||
|
||||
// Delete existing service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-service-source", "Delete an existing service source", getServiceSourceSchema()),
|
||||
handleDeleteServiceSource(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListServiceSources(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/service-sources")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list service sources: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in configurations")
|
||||
}
|
||||
if _, ok := configurations["type"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'type' in configurations")
|
||||
}
|
||||
if _, ok := configurations["domain"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'domain' in configurations")
|
||||
}
|
||||
if _, ok := configurations["port"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/service-sources", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add service source: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Get current service source configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current service source configuration: %w", err)
|
||||
}
|
||||
|
||||
var response ServiceSourceResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current service source response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
|
||||
// Update configurations using JSON marshal/unmarshal for type conversion
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig ServiceSource
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse service source configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Name != "" {
|
||||
currentConfig.Name = newConfig.Name
|
||||
}
|
||||
if newConfig.Type != "" {
|
||||
currentConfig.Type = newConfig.Type
|
||||
}
|
||||
if newConfig.Domain != "" {
|
||||
currentConfig.Domain = newConfig.Domain
|
||||
}
|
||||
if newConfig.Port != 0 {
|
||||
currentConfig.Port = newConfig.Port
|
||||
}
|
||||
if newConfig.Protocol != "" {
|
||||
currentConfig.Protocol = newConfig.Protocol
|
||||
}
|
||||
if newConfig.SNI != nil {
|
||||
currentConfig.SNI = newConfig.SNI
|
||||
}
|
||||
if newConfig.Properties != nil {
|
||||
currentConfig.Properties = newConfig.Properties
|
||||
}
|
||||
if newConfig.AuthN != nil {
|
||||
currentConfig.AuthN = newConfig.AuthN
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source to retrieve"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getAddServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address (required)"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"description": "The port number (required)"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections (optional)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "domain", "port"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source to update"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"description": "The port number"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package higress
|
||||
|
||||
// APIResponse represents the standard Higress API response format
|
||||
type APIResponse[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data T `json:"data,omitempty"`
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
# MCP Server
|
||||
English | [简体中文](./README.md)
|
||||
|
||||
## Overview
|
||||
|
||||
MCP Server is a Golang Filter plugin based on Envoy, designed to implement Server-Sent Events (SSE) and message communication functionality. This plugin supports various database types and uses Redis as a message queue to enable load-balanced requests to be sent through corresponding SSE connections.
|
||||
|
||||
> **Note**: MCP Server requires Higress 2.1.0 or higher version.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # Configuration parsing code
|
||||
├── filter.go # Request processing code
|
||||
├── internal/ # Internal implementation logic
|
||||
├── servers/ # MCP server implementation
|
||||
├── go.mod # Go module dependency definition
|
||||
└── go.sum # Go module dependency checksum
|
||||
```
|
||||
|
||||
## MCP Server Development Guide
|
||||
|
||||
```go
|
||||
// Register your server in the init function
|
||||
// Param 1: Server name
|
||||
// Param 2: Config struct instance
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// Server configuration struct
|
||||
type DemoConfig struct {
|
||||
helloworld string
|
||||
}
|
||||
|
||||
// Configuration parsing method
|
||||
// Parse and validate configuration items from the config map
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
helloworld, ok := config["helloworld"].(string)
|
||||
if !ok { return errors.New("missing helloworld")}
|
||||
c.helloworld = helloworld
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new MCP server instance
|
||||
// serverName: Server name
|
||||
// Returns: MCP server instance and possible error
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
|
||||
// Add tool methods to server
|
||||
// mcpServer.AddTool()
|
||||
|
||||
// Add resources to server
|
||||
// mcpServer.AddResource()
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**:
|
||||
Need to use underscore import in config.go to execute the package's init function
|
||||
```go
|
||||
import (
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
)
|
||||
```
|
||||
@@ -3,24 +3,36 @@ package common
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// RuleType defines the type of matching rule
|
||||
type RuleType string
|
||||
|
||||
// UpstreamType defines the type of matching rule
|
||||
type UpstreamType string
|
||||
|
||||
const (
|
||||
ExactMatch RuleType = "exact"
|
||||
PrefixMatch RuleType = "prefix"
|
||||
SuffixMatch RuleType = "suffix"
|
||||
ContainsMatch RuleType = "contains"
|
||||
RegexMatch RuleType = "regex"
|
||||
|
||||
RestUpstream UpstreamType = "rest"
|
||||
SSEUpstream UpstreamType = "sse"
|
||||
StreamableUpstream UpstreamType = "streamable"
|
||||
)
|
||||
|
||||
// MatchRule defines the structure for a matching rule
|
||||
type MatchRule struct {
|
||||
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
|
||||
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
|
||||
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
UpstreamType UpstreamType `json:"upstream_type"` // Type of upstream(s) matched by the rule
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite"` // Enable request path rewrite for matched routes
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix"` // Prefix the request path would be rewritten to.
|
||||
}
|
||||
|
||||
// ParseMatchList parses the match list from the config
|
||||
@@ -38,6 +50,34 @@ func ParseMatchList(matchListConfig []interface{}) []MatchRule {
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = RuleType(ruleType)
|
||||
}
|
||||
if upstreamType, ok := ruleMap["upstream_type"].(string); ok {
|
||||
rule.UpstreamType = UpstreamType(upstreamType)
|
||||
}
|
||||
if len(rule.UpstreamType) == 0 {
|
||||
rule.UpstreamType = RestUpstream
|
||||
} else {
|
||||
switch rule.UpstreamType {
|
||||
case RestUpstream, SSEUpstream, StreamableUpstream:
|
||||
break
|
||||
default:
|
||||
api.LogWarnf("Unknown upstream type: %s", rule.UpstreamType)
|
||||
}
|
||||
}
|
||||
if enablePathRewrite, ok := ruleMap["enable_path_rewrite"].(bool); ok {
|
||||
rule.EnablePathRewrite = enablePathRewrite
|
||||
}
|
||||
if pathRewritePrefix, ok := ruleMap["path_rewrite_prefix"].(string); ok {
|
||||
rule.PathRewritePrefix = pathRewritePrefix
|
||||
}
|
||||
if rule.EnablePathRewrite {
|
||||
if rule.UpstreamType != SSEUpstream {
|
||||
api.LogWarnf("Path rewrite is only supported for SSE upstream type")
|
||||
} else if rule.MatchRuleType != PrefixMatch {
|
||||
api.LogWarnf("Path rewrite is only supported for prefix match type")
|
||||
} else if !strings.HasPrefix(rule.PathRewritePrefix, "/") {
|
||||
rule.PathRewritePrefix = "/" + rule.PathRewritePrefix
|
||||
}
|
||||
}
|
||||
matchList = append(matchList, rule)
|
||||
}
|
||||
}
|
||||
@@ -96,17 +136,17 @@ func matchDomainAndPath(domain, path string, rule MatchRule) bool {
|
||||
|
||||
// IsMatch checks if the request matches any rule in the rule list
|
||||
// Returns true if no rules are specified
|
||||
func IsMatch(rules []MatchRule, host, path string) bool {
|
||||
func IsMatch(rules []MatchRule, host, path string) (bool, MatchRule) {
|
||||
if len(rules) == 0 {
|
||||
return true
|
||||
return true, MatchRule{}
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if matchDomainAndPath(host, path, rule) {
|
||||
return true
|
||||
return true, rule
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, MatchRule{}
|
||||
}
|
||||
|
||||
// MatchDomainList checks if the domain matches any of the domains in the list
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -94,13 +95,15 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
defer s.sessions.Delete(sessionID)
|
||||
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
u, err := url.Parse(s.baseURL + s.messageEndpoint)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to parse base URL: %v", err)
|
||||
}
|
||||
|
||||
messageEndpoint := fmt.Sprintf(
|
||||
"%s%s?sessionId=%s",
|
||||
s.baseURL,
|
||||
s.messageEndpoint,
|
||||
sessionID,
|
||||
)
|
||||
q := u.Query()
|
||||
q.Set("sessionId", sessionID)
|
||||
u.RawQuery = q.Encode()
|
||||
messageEndpoint := u.String()
|
||||
|
||||
// go func() {
|
||||
// for {
|
||||
@@ -126,7 +129,7 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
// }
|
||||
// }()
|
||||
|
||||
err := s.redisClient.Subscribe(channel, stopChan, func(message string) {
|
||||
err = s.redisClient.Subscribe(channel, stopChan, func(message string) {
|
||||
defer cb.EncoderFilterCallbacks().RecoverPanic()
|
||||
api.LogDebugf("SSE Send message: %s", message)
|
||||
cb.EncoderFilterCallbacks().InjectData([]byte(message))
|
||||
@@ -136,7 +139,7 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
}
|
||||
|
||||
// Send the initial endpoint event
|
||||
initialEvent := fmt.Sprintf("event: endpoint\ndata: %s\r\n\r\n", messageEndpoint)
|
||||
initialEvent := fmt.Sprintf("event: endpoint\ndata: %s\n\n", messageEndpoint)
|
||||
err = s.redisClient.Publish(channel, initialEvent)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to send initial event: %v", err)
|
||||
@@ -210,7 +213,7 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j
|
||||
var status int
|
||||
// Only send response if there is one (not for notifications)
|
||||
if response != nil {
|
||||
if sessionID != ""{
|
||||
if sessionID != "" {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
redisClient, err := common.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to initialize Redis client: %w", err)
|
||||
api.LogErrorf("Failed to initialize Redis client: %v", err)
|
||||
} else {
|
||||
api.LogDebug("Redis client initialized")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package mcp_session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -28,10 +29,14 @@ type filter struct {
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
proxyURL *url.URL
|
||||
neepProcess bool
|
||||
req *http.Request
|
||||
serverName string
|
||||
proxyURL *url.URL
|
||||
matchedRule common.MatchRule
|
||||
needProcess bool
|
||||
skipRequestBody bool
|
||||
skipResponseBody bool
|
||||
cachedResponseBody []byte
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
@@ -42,31 +47,33 @@ type filter struct {
|
||||
// Callbacks which are called in request path
|
||||
// The endStream is true if the request doesn't have body
|
||||
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
url := common.NewRequestURL(header)
|
||||
if url == nil {
|
||||
requestUrl := common.NewRequestURL(header)
|
||||
if requestUrl == nil {
|
||||
return api.Continue
|
||||
}
|
||||
f.path = url.ParsedURL.Path
|
||||
f.path = requestUrl.ParsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
if !common.IsMatch(f.config.matchList, url.Host, f.path) {
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", url.ParsedURL.String())
|
||||
matched, matchedRule := common.IsMatch(f.config.matchList, requestUrl.Host, f.path)
|
||||
if !matched {
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", requestUrl.ParsedURL.String())
|
||||
return api.Continue
|
||||
}
|
||||
f.neepProcess = true
|
||||
f.needProcess = true
|
||||
f.matchedRule = matchedRule
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.Method,
|
||||
URL: url.ParsedURL,
|
||||
Method: requestUrl.Method,
|
||||
URL: requestUrl.ParsedURL,
|
||||
}
|
||||
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
|
||||
if !url.InternalIP {
|
||||
api.LogWarnf("Access denied: non-Internal IP address %s", url.ParsedURL.String())
|
||||
if !requestUrl.InternalIP {
|
||||
api.LogWarnf("Access denied: non-Internal IP address %s", requestUrl.ParsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.Method == http.MethodGet {
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && requestUrl.Method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
@@ -79,10 +86,27 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix) {
|
||||
f.proxyURL = url.ParsedURL
|
||||
return f.processMcpRequestHeaders(header, endStream)
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
switch f.matchedRule.UpstreamType {
|
||||
case common.RestUpstream, common.StreamableUpstream:
|
||||
return f.processMcpRequestHeadersForRestUpstream(header, endStream)
|
||||
case common.SSEUpstream:
|
||||
return f.processMcpRequestHeadersForSSEUpstream(header, endStream)
|
||||
}
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeadersForRestUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
method := f.req.Method
|
||||
requestUrl := f.req.URL
|
||||
if !strings.HasSuffix(requestUrl.Path, GlobalSSEPathSuffix) {
|
||||
f.proxyURL = requestUrl
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.ParsedURL.Path, "/")
|
||||
parts := strings.Split(requestUrl.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
@@ -102,12 +126,18 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
|
||||
if url.Method != http.MethodGet {
|
||||
if method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
// to support the query param in Message Endpoint
|
||||
trimmed := strings.TrimSuffix(requestUrl.Path, GlobalSSEPathSuffix)
|
||||
if rq := requestUrl.RawQuery; rq != "" {
|
||||
trimmed += "?" + rq
|
||||
}
|
||||
|
||||
f.config.defaultServer = common.NewSSEServer(common.NewMCPServer(DefaultServerName, Version),
|
||||
common.WithSSEEndpoint(GlobalSSEPathSuffix),
|
||||
common.WithMessageEndpoint(strings.TrimSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix)),
|
||||
common.WithMessageEndpoint(trimmed),
|
||||
common.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
@@ -116,10 +146,19 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
return api.LocalReply
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeadersForSSEUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
// We don't need to process the request body for SSE upstream.
|
||||
f.skipRequestBody = true
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// DecodeData might be called multiple times during handling the request body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
if !f.needProcess || f.skipRequestBody {
|
||||
return api.Continue
|
||||
}
|
||||
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
@@ -158,10 +197,17 @@ func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// Callbacks which are called in response path
|
||||
// The endStream is true if the response doesn't have body
|
||||
// EncodeHeaders Callbacks which are called in response path.
|
||||
// The endStream is true if the response doesn't have body.
|
||||
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
if !f.needProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
|
||||
if contentType, ok := header.Get("content-type"); !ok || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
api.LogDebugf("Skip response body for non-SSE upstream. Content-Type: %s", contentType)
|
||||
f.skipResponseBody = true
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
@@ -182,7 +228,30 @@ func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api
|
||||
// EncodeData might be called multiple times during handling the response body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
if !f.needProcess || f.skipResponseBody {
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
ret := api.Continue
|
||||
api.LogDebugf("Upstream Type: %s", f.matchedRule.UpstreamType)
|
||||
switch f.matchedRule.UpstreamType {
|
||||
case common.RestUpstream, common.StreamableUpstream:
|
||||
api.LogDebugf("Encoding data from Rest upstream")
|
||||
ret = f.encodeDataFromRestUpstream(buffer, endStream)
|
||||
break
|
||||
case common.SSEUpstream:
|
||||
api.LogDebugf("Encoding data from SSE upstream")
|
||||
ret = f.encodeDataFromSSEUpstream(buffer, endStream)
|
||||
if endStream {
|
||||
// Always continue as long as the stream has ended.
|
||||
ret = api.Continue
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *filter) encodeDataFromRestUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.needProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
@@ -207,13 +276,216 @@ func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
_ = buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) encodeDataFromSSEUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
bufferBytes := buffer.Bytes()
|
||||
bufferData := string(bufferBytes)
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(bufferData)
|
||||
if err != nil {
|
||||
api.LogWarnf("Failed to find endpoint URL in SSE data: %v", err)
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
if endpointUrl == "" {
|
||||
// No endpoint URL found. Need to buffer and check again.
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
|
||||
// Remove query string since we don't need to change it.
|
||||
queryStringIndex := strings.IndexAny(endpointUrl, "?")
|
||||
if queryStringIndex != -1 {
|
||||
endpointUrl = endpointUrl[:queryStringIndex]
|
||||
}
|
||||
|
||||
if changed, newEndpointUrl := f.rewriteEndpointUrl(endpointUrl); changed {
|
||||
api.LogDebugf("The endpoint URL is changed.\n Old: %s\n New: %s", endpointUrl, newEndpointUrl)
|
||||
|
||||
endpointUrlIndex := strings.Index(bufferData, endpointUrl)
|
||||
if endpointUrlIndex == -1 {
|
||||
api.LogWarnf("Something wrong, the previously found endpoint URL %s not found in the SSE data now", endpointUrl)
|
||||
} else {
|
||||
bufferData = bufferData[:endpointUrlIndex] + newEndpointUrl + bufferData[endpointUrlIndex+len(endpointUrl):]
|
||||
_ = buffer.SetString(bufferData)
|
||||
}
|
||||
} else {
|
||||
api.LogDebugf("The endpoint URL %s is not changed", endpointUrl)
|
||||
}
|
||||
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) rewriteEndpointUrl(endpointUrl string) (bool, string) {
|
||||
if !f.matchedRule.EnablePathRewrite {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if schemeIndex := strings.Index(endpointUrl, "://"); schemeIndex != -1 {
|
||||
endpointUrl = endpointUrl[schemeIndex+3:]
|
||||
if slashIndex := strings.Index(endpointUrl, "/"); slashIndex != -1 {
|
||||
endpointUrl = endpointUrl[slashIndex:]
|
||||
} else {
|
||||
endpointUrl = "/"
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpointUrl, f.matchedRule.PathRewritePrefix) {
|
||||
// The endpoint URL does not match the path rewrite prefix. We are unable to rewrite it back.
|
||||
api.LogWarnf("The endpoint URL %s does not match the path rewrite prefix %s", endpointUrl, f.matchedRule.PathRewritePrefix)
|
||||
return false, ""
|
||||
}
|
||||
|
||||
suffix := endpointUrl[len(f.matchedRule.PathRewritePrefix):]
|
||||
|
||||
if len(suffix) == 0 {
|
||||
endpointUrl = f.matchedRule.MatchRulePath
|
||||
} else {
|
||||
matchPathHasTrailingSlash := strings.HasSuffix(f.matchedRule.MatchRulePath, "/")
|
||||
suffixHasLeadingSlash := strings.HasPrefix(suffix, "/")
|
||||
if matchPathHasTrailingSlash != suffixHasLeadingSlash {
|
||||
// One has, the other doesn't have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + suffix
|
||||
} else if matchPathHasTrailingSlash {
|
||||
// Both have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + suffix[1:]
|
||||
} else {
|
||||
// Neither have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + "/" + suffix
|
||||
}
|
||||
}
|
||||
|
||||
return true, endpointUrl
|
||||
}
|
||||
|
||||
func (f *filter) findNextLineBreak(bufferData string) (error, string) {
|
||||
// See https://html.spec.whatwg.org/multipage/server-sent-events.html
|
||||
crIndex := strings.IndexAny(bufferData, "\r")
|
||||
lfIndex := strings.IndexAny(bufferData, "\n")
|
||||
if crIndex == -1 && lfIndex == -1 {
|
||||
// No line break found.
|
||||
return nil, ""
|
||||
}
|
||||
lineBreak := ""
|
||||
if crIndex != -1 && lfIndex != -1 {
|
||||
if crIndex < lfIndex {
|
||||
if crIndex+1 == lfIndex {
|
||||
lineBreak = "\r\n"
|
||||
} else {
|
||||
lineBreak = "\r"
|
||||
}
|
||||
} else {
|
||||
if crIndex == lfIndex+1 {
|
||||
// Found unexpected "\n\r". Skip body processing.
|
||||
return errors.New("found unexpected LF+CR"), ""
|
||||
} else {
|
||||
lineBreak = "\n"
|
||||
}
|
||||
}
|
||||
} else if crIndex != -1 {
|
||||
lineBreak = "\r"
|
||||
} else {
|
||||
lineBreak = "\n"
|
||||
}
|
||||
return nil, lineBreak
|
||||
}
|
||||
|
||||
func (f *filter) findEndpointUrl(bufferData string) (error, string) {
|
||||
// Keep searching for events until we find an endpoint event or run out of data
|
||||
for {
|
||||
eventIndex := strings.Index(bufferData, "event:")
|
||||
if eventIndex == -1 {
|
||||
// No more events found
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Move to the start of the event
|
||||
bufferData = bufferData[eventIndex:]
|
||||
|
||||
// Find the end of the event line
|
||||
err, lineBreak := f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
api.LogDebugf("event line break sequence: %v", []byte(lineBreak))
|
||||
eventEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if eventEndIndex == -1 {
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
eventName := strings.TrimSpace(bufferData[len("event:"):eventEndIndex])
|
||||
|
||||
// Move past the event line
|
||||
bufferData = bufferData[eventEndIndex+len(lineBreak):]
|
||||
|
||||
if eventName == "endpoint" {
|
||||
// Found endpoint event, now look for the data field
|
||||
err, lineBreak = f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
api.LogDebugf("data line break sequence: %v", []byte(lineBreak))
|
||||
dataEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if dataEndIndex == -1 {
|
||||
// Data received not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
eventData := bufferData[:dataEndIndex]
|
||||
if !strings.HasPrefix(eventData, "data:") {
|
||||
return fmt.Errorf("an unexpected non-data field found in the event. Skip processing. Field: %s", eventData), ""
|
||||
}
|
||||
|
||||
return nil, strings.TrimSpace(eventData[len("data:"):])
|
||||
} else {
|
||||
// Not an endpoint event, skip to the next event
|
||||
api.LogDebugf("Skipping non-endpoint event: %s", eventName)
|
||||
|
||||
// First, we need to skip the data field of this event
|
||||
err, lineBreak = f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
dataEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if dataEndIndex == -1 {
|
||||
// Data received not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Move past the data line
|
||||
bufferData = bufferData[dataEndIndex+len(lineBreak):]
|
||||
|
||||
// Skip any additional empty lines that separate events
|
||||
for strings.HasPrefix(bufferData, lineBreak) {
|
||||
bufferData = bufferData[len(lineBreak):]
|
||||
}
|
||||
|
||||
// Continue to look for the next event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
|
||||
464
plugins/golang-filter/mcp-session/filter_test.go
Normal file
464
plugins/golang-filter/mcp-session/filter_test.go
Normal file
@@ -0,0 +1,464 @@
|
||||
package mcp_session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// Mock implementation of CommonCAPI for testing
|
||||
type mockCommonCAPI struct {
|
||||
logs []string
|
||||
}
|
||||
|
||||
func (m *mockCommonCAPI) Log(level api.LogType, message string) {
|
||||
fmt.Printf("[%s] %s", level, message)
|
||||
m.logs = append(m.logs, message)
|
||||
}
|
||||
|
||||
func (m *mockCommonCAPI) LogLevel() api.LogType {
|
||||
return api.Debug
|
||||
}
|
||||
|
||||
// Test helper to create a filter instance for testing
|
||||
func createTestFilter() *filter {
|
||||
return &filter{}
|
||||
}
|
||||
|
||||
// Test helper to create a match rule for testing
|
||||
func createTestMatchRule() common.MatchRule {
|
||||
return common.MatchRule{
|
||||
UpstreamType: common.SSEUpstream,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/api/v1",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: common.PrefixMatch,
|
||||
MatchRuleDomain: "example.com",
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_ValidEndpointMessage tests the current behavior with a valid endpoint message
|
||||
func TestFindEndpointUrl_ValidEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with valid endpoint message
|
||||
sseData := "event: endpoint\ndata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_NonEndpointFirstMessage tests improved behavior with non-endpoint first message
|
||||
func TestFindEndpointUrl_NonEndpointFirstMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with ping message first (this should now succeed with improved implementation)
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\ndata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
// Improved implementation should handle non-endpoint first message
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for non-endpoint first message, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
|
||||
// Check that the non-endpoint event was logged
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == "Skipping non-endpoint event: ping" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message about skipping ping event not found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_MultipleNonEndpointMessages tests multiple non-endpoint messages before endpoint
|
||||
func TestFindEndpointUrl_MultipleNonEndpointMessages(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with multiple non-endpoint messages before endpoint
|
||||
sseData := "event: ping\ndata: alive\n\nevent: status\ndata: connecting\n\nevent: info\ndata: ready\n\nevent: endpoint\ndata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
|
||||
// Check that all non-endpoint events were logged
|
||||
expectedLogs := []string{
|
||||
"Skipping non-endpoint event: ping",
|
||||
"Skipping non-endpoint event: status",
|
||||
"Skipping non-endpoint event: info",
|
||||
}
|
||||
|
||||
for _, expectedLog := range expectedLogs {
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == expectedLog {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message '%s' not found", expectedLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_EndpointInMiddle tests endpoint message in the middle of other messages
|
||||
func TestFindEndpointUrl_EndpointInMiddle(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with endpoint message in the middle
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\ndata: https://api.example.com/chat\n\nevent: status\ndata: ready\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
|
||||
// Check that the ping event was logged as skipped
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == "Skipping non-endpoint event: ping" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message about skipping ping event not found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_NoEndpointMessage tests when no endpoint message is present
|
||||
func TestFindEndpointUrl_NoEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with no endpoint message
|
||||
sseData := "event: ping\ndata: alive\n\nevent: status\ndata: connecting\n\nevent: info\ndata: ready\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when no endpoint found, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when no endpoint found, got '%s'", endpointUrl)
|
||||
}
|
||||
|
||||
// Check that all non-endpoint events were logged
|
||||
expectedLogs := []string{
|
||||
"Skipping non-endpoint event: ping",
|
||||
"Skipping non-endpoint event: status",
|
||||
"Skipping non-endpoint event: info",
|
||||
}
|
||||
|
||||
for _, expectedLog := range expectedLogs {
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == expectedLog {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message '%s' not found", expectedLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_IncompleteEndpointMessage tests incomplete endpoint message
|
||||
func TestFindEndpointUrl_IncompleteEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with incomplete endpoint message (missing final line break)
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\ndata: https://api.example.com/chat"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for incomplete endpoint message, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL for incomplete message, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_IncompleteNonEndpointMessage tests incomplete non-endpoint message
|
||||
func TestFindEndpointUrl_IncompleteNonEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with incomplete non-endpoint message
|
||||
sseData := "event: ping\ndata: alive"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for incomplete non-endpoint message, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL for incomplete message, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_MalformedEndpointData tests malformed endpoint data
|
||||
func TestFindEndpointUrl_MalformedEndpointData(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with malformed endpoint data (missing data field)
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\nnotdata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
// Should return error for malformed endpoint data
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for malformed endpoint data, but got none")
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when error occurs, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_DifferentLineBreaks tests different line break formats with improved version
|
||||
func TestFindEndpointUrl_DifferentLineBreaks(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sseData string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "CRLF line breaks with ping first",
|
||||
sseData: "event: ping\r\ndata: alive\r\n\r\nevent: endpoint\r\ndata: https://api.example.com/chat\r\n\r\n",
|
||||
expected: "https://api.example.com/chat",
|
||||
},
|
||||
{
|
||||
name: "CR line breaks with status first",
|
||||
sseData: "event: status\rdata: ready\r\revent: endpoint\rdata: https://api.example.com/chat\r\r",
|
||||
expected: "https://api.example.com/chat",
|
||||
},
|
||||
{
|
||||
name: "LF line breaks with info first",
|
||||
sseData: "event: info\ndata: starting\n\nevent: endpoint\ndata: https://api.example.com/chat\n\n",
|
||||
expected: "https://api.example.com/chat",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(tc.sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != tc.expected {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", tc.expected, endpointUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_WithWhitespace tests improved version with whitespace
|
||||
func TestFindEndpointUrl_WithWhitespace(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with whitespace around event names and data
|
||||
sseData := "event: ping \ndata: alive \n\nevent: endpoint \ndata: https://api.example.com/chat \n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_NoEventFound tests behavior when no event is found
|
||||
func TestFindEndpointUrl_NoEventFound(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with data that doesn't contain event
|
||||
sseData := "some random data without event"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when no event found, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when no event found, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_MalformedData tests behavior with malformed SSE data
|
||||
func TestFindEndpointUrl_MalformedData(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with malformed data (missing data field)
|
||||
sseData := "event: endpoint\nnotdata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
// Should return error for malformed data
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for malformed data, but got none")
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when error occurs, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindNextLineBreak tests the line break detection functionality
|
||||
func TestFindNextLineBreak(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedBreak string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "LF only",
|
||||
input: "some text\nmore text",
|
||||
expectedBreak: "\n",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "CR only",
|
||||
input: "some text\rmore text",
|
||||
expectedBreak: "\r",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "CRLF",
|
||||
input: "some text\r\nmore text",
|
||||
expectedBreak: "\r\n",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "No line break",
|
||||
input: "some text without break",
|
||||
expectedBreak: "",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "LF before CR (separate)",
|
||||
input: "some text\n\rmore text",
|
||||
expectedBreak: "",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err, lineBreak := f.findNextLineBreak(tc.input)
|
||||
|
||||
if tc.expectedError && err == nil {
|
||||
t.Errorf("Expected error, but got none")
|
||||
}
|
||||
|
||||
if !tc.expectedError && err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if lineBreak != tc.expectedBreak {
|
||||
t.Errorf("Expected line break '%v', got '%v'", []byte(tc.expectedBreak), []byte(lineBreak))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,6 @@ build:gcc --cxxopt=-std=c++17
|
||||
build:clang --action_env=CC=clang --action_env=CXX=clang++
|
||||
build:clang --action_env=BAZEL_COMPILER=clang
|
||||
build:clang --linkopt=-fuse-ld=lld
|
||||
build:clang --cxxopt=-std=c++17
|
||||
build:clang --cxxopt=-std=c++17
|
||||
|
||||
build --incompatible_use_platforms_repo_for_constraints=false
|
||||
@@ -1 +1 @@
|
||||
5.4.0
|
||||
6.0.0
|
||||
@@ -1,6 +1,13 @@
|
||||
workspace(name = "istio_ecosystem_wasm_extensions")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
http_archive(
|
||||
name = "platforms",
|
||||
url = "https://github.com/bazelbuild/platforms/releases/download/0.0.9/platforms-0.0.9.tar.gz",
|
||||
sha256 = "5eda539c841265031c2f82d8ae7a3a6490bd62176e0c038fc469eabf91f6149b",
|
||||
)
|
||||
|
||||
load("//bazel:third_party.bzl", "wasm_extension_dependency")
|
||||
|
||||
wasm_extension_dependency()
|
||||
@@ -16,9 +23,9 @@ load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
|
||||
|
||||
container_deps()
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA = "eaec483b5b3c7bcb89fd208b5a1fa5d79d626f61"
|
||||
PROXY_WASM_CPP_SDK_SHA = "0ceca8c81dddc4c9875cf0cb997454764905658c"
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "1140bc8114d75db56a6ca6b18423d4df50d988d40b4cec929a1eb246cf5a4a3d"
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "cb010b242d49fb02b39124421b6acb69bd4ece64fb6299ba3f98f3b36eef7004"
|
||||
|
||||
http_archive(
|
||||
name = "proxy_wasm_cpp_sdk",
|
||||
|
||||
@@ -202,7 +202,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
}
|
||||
item = consumer.find("keys");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("not found keys configuration for consumer " + c.name + ", will use global configuration to extract keys");
|
||||
LOG_DEBUG("not found keys configuration for consumer " + c.name + ", will use global configuration to extract keys");
|
||||
need_global_keys = true;
|
||||
} else {
|
||||
c.keys = std::vector<std::string>{OriginalAuthKey};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `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"] | 只对这些特定路径后缀的请求生效 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations","/image-synthesis","/video-synthesis"] | 只对这些特定路径后缀的请求生效|
|
||||
|
||||
|
||||
## 效果说明
|
||||
|
||||
@@ -7,7 +7,7 @@ The `model-mapper` plugin implements the functionality of routing based on the m
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `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. |
|
||||
| `enableOnPathSuffix` | array of string | Optional | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations","/image-synthesis","/video-synthesis"] | Only applies to requests with these specific path suffixes. |
|
||||
|
||||
## Runtime Properties
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ struct ModelMapperConfigRule {
|
||||
std::string default_model_mapping_;
|
||||
std::vector<std::string> enable_on_path_suffix_ = {
|
||||
"/completions", "/embeddings", "/images/generations",
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations",
|
||||
"/image-synthesis", "/video-synthesis"};
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
|
||||
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations","/image-synthesis","/video-synthesis"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
|
||||
## 运行属性
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The `model-router` plugin implements routing functionality based on the model pa
|
||||
| `modelKey` | string | Optional | model | Location of the model parameter in the request body |
|
||||
| `addProviderHeader` | string | Optional | - | Which request header to add the provider name parsed from the model parameter |
|
||||
| `modelToHeader` | string | Optional | - | Which request header to directly add the model parameter to |
|
||||
| `enableOnPathSuffix` | array of string | Optional | ["/v1/chat/completions"] | Only effective for requests with these specific path suffixes, can be configured as "*" to match all paths |
|
||||
| `enableOnPathSuffix` | array of string | Optional | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations","/image-synthesis","/video-synthesis"] | Only effective for requests with these specific path suffixes, can be configured as "*" to match all paths |
|
||||
|
||||
## Runtime Properties
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ struct ModelRouterConfigRule {
|
||||
std::string model_to_header_;
|
||||
std::vector<std::string> enable_on_path_suffix_ = {
|
||||
"/completions", "/embeddings", "/images/generations",
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations",
|
||||
"/image-synthesis", "/video-synthesis"};
|
||||
};
|
||||
|
||||
class PluginContext;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.20.14-tinygo0.29.0-oras1.0.0
|
||||
FROM $BUILDER as builder
|
||||
ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.24.0-oras1.0.0
|
||||
FROM $BUILDER AS builder
|
||||
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY}
|
||||
|
||||
ARG EXTRA_TAGS=""
|
||||
ENV EXTRA_TAGS=${EXTRA_TAGS}
|
||||
|
||||
ARG PLUGIN_NAME=hello-world
|
||||
|
||||
WORKDIR /workspace
|
||||
@@ -18,14 +15,8 @@ WORKDIR /workspace/extensions/$PLUGIN_NAME
|
||||
|
||||
RUN go mod tidy
|
||||
RUN \
|
||||
if echo "$PLUGIN_NAME" | grep -Eq '^waf$'; then \
|
||||
# Please use higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.19-tinygo0.28.1-oras1.0.0 as BUILDER
|
||||
go run mage.go build && \
|
||||
mv ./local/main.wasm /main.wasm ; \
|
||||
else \
|
||||
tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./ ; \
|
||||
fi
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o /main.wasm ./
|
||||
|
||||
FROM scratch as output
|
||||
FROM scratch AS output
|
||||
|
||||
COPY --from=builder /main.wasm plugin.wasm
|
||||
|
||||
@@ -1,115 +1,35 @@
|
||||
# The Dockerfile for wasm-go builder only support amd64 and arm64 yet.
|
||||
# If you want to build on another architecture, the following information may be helpful.
|
||||
#
|
||||
# - arch: amd64
|
||||
# base image: docker.io/ubuntu
|
||||
# go_url: https://golang.google.cn/dl/go1.20.1.linux-amd64.tar.gz"
|
||||
# tinygo_url: https://github.com/alibaba/higress/releases/download/v1.0.0-rc/higress-tinygo0.25.0.linux-amd64.tar.gz
|
||||
# oras_url: https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
|
||||
#
|
||||
# - arch: arm64
|
||||
# base image: docker.io/ubuntu
|
||||
# go_url: https://golang.google.cn/dl/go1.20.1.linux-arm64.tar.gz
|
||||
# tinygo_url: https://github.com/alibaba/higress/releases/download/v1.0.0-rc/higress-tinygo0.25.0.linux-arm64.tar.gz
|
||||
# oras_url: https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_arm64.tar.gz
|
||||
#
|
||||
# - arch: armel
|
||||
# base image: build yourself
|
||||
# go_url: install from source code
|
||||
# tinygo_url: build yourself
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: i386
|
||||
# base image: build yourself
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-386.tar.gz
|
||||
# tinygo_url: build yourself
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: mips64el
|
||||
# base image: build your self
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-386.tar.gz
|
||||
# tinygo_url: build your self
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: ppc64el
|
||||
# base image: build your self
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-ppc64le.tar.gz
|
||||
# tinygo_url: build your self
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: s390x
|
||||
# base image: docker.io/ubuntu
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-s390x.tar.gz
|
||||
# tinygo_url: build your self
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: armhf
|
||||
# base image: build your self
|
||||
# go_url: https://golang.google.cn/dl/go1.20.1.linux-armv6l.tar.gz
|
||||
# tinygo_url: https://github.com/tinygo-org/tinygo/releases/download/v0.25.0/tinygo_0.25.0_armhf.deb
|
||||
# oras_url: build your self
|
||||
|
||||
ARG BASE_IMAGE=docker.io/ubuntu
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG GO_VERSION
|
||||
ARG TINYGO_VERSION
|
||||
ARG ORAS_VERSION
|
||||
ARG HIGRESS_VERSION
|
||||
ARG USE_HIGRESS_TINYGO
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG ORAS_VERSION=1.0.0
|
||||
ARG HIGRESS_VERSION=1.0.0-rc
|
||||
|
||||
LABEL go_version=$GO_VERSION tinygo_version=$TINYGO_VERSION oras_version=$ORAS_VERSION
|
||||
LABEL go_version=$GO_VERSION oras_version=$ORAS_VERSION
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y wget tar && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
go_url=; \
|
||||
tinygo_url=; \
|
||||
go_version=${GO_VERSION:-1.19}; \
|
||||
tinygo_version=${TINYGO_VERSION:-0.25.0}; \
|
||||
oras_version=${ORAS_VERSION:-1.0.0}; \
|
||||
higress_version=${HIGRESS_VERSION:-1.0.0-rc}; \
|
||||
use_higress_tinygo=${USE_HIGRESS_TINYGO:-false}; \
|
||||
echo "arch: '$arch'"; \
|
||||
echo "go go_version: '$go_version'"; \
|
||||
echo "tinygo_version: '$tinygo_version'"; \
|
||||
echo "oras_version: '$oras_version'"; \
|
||||
echo "higress_version: '$higress_version'"; \
|
||||
echo "use_higress_tinygo: '$use_higress_tinygo'"; \
|
||||
RUN set -e; \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
echo "arch: $arch"; \
|
||||
go_url=""; tinygo_url=""; \
|
||||
case "$arch" in \
|
||||
'amd64') \
|
||||
go_url="https://golang.google.cn/dl/go$go_version.linux-amd64.tar.gz"; \
|
||||
if [ "$use_higress_tinygo" = "true" ]; \
|
||||
then \
|
||||
tinygo_url="https://github.com/alibaba/higress/releases/download/v$higress_version/higress-tinygo${tinygo_version}.linux-amd64.tar.gz"; \
|
||||
else \
|
||||
tinygo_url="https://github.com/tinygo-org/tinygo/releases/download/v$tinygo_version/tinygo${tinygo_version}.linux-amd64.tar.gz"; \
|
||||
fi; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v$oras_version/oras_${oras_version}_linux_amd64.tar.gz"; \
|
||||
;; \
|
||||
'arm64') \
|
||||
go_url="https://golang.google.cn/dl/go$go_version.linux-arm64.tar.gz"; \
|
||||
if [ "$use_higress_tinygo" = "true" ]; \
|
||||
then \
|
||||
tinygo_url="https://github.com/alibaba/higress/releases/download/v$higress_version/higress-tinygo${tinygo_version}.linux-arm64.tar.gz"; \
|
||||
else \
|
||||
tinygo_url="https://github.com/tinygo-org/tinygo/releases/download/v$tinygo_version/tinygo${tinygo_version}.linux-arm64.tar.gz"; \
|
||||
fi; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v$oras_version/oras_${oras_version}_linux_arm64.tar.gz"; \
|
||||
;; \
|
||||
*) echo >&2 "error: unsupported architecture '$arch' "; exit 1 ;; \
|
||||
'amd64') \
|
||||
go_url="https://golang.google.cn/dl/go${GO_VERSION}.linux-amd64.tar.gz"; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz"; \
|
||||
;; \
|
||||
'arm64') \
|
||||
go_url="https://golang.google.cn/dl/go${GO_VERSION}.linux-arm64.tar.gz"; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_arm64.tar.gz"; \
|
||||
;; \
|
||||
*) echo >&2 "unsupported architecture: $arch"; exit 1 ;; \
|
||||
esac; \
|
||||
echo "go_url: '$go_url'"; \
|
||||
wget -O go.tgz "$go_url" --progress=dot:giga; \
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go.tgz && rm -rf go.tgz; \
|
||||
echo "tinygo_url: '$tinygo_url'"; \
|
||||
wget -O tinygo.tgz "$tinygo_url" --progress=dot:giga; \
|
||||
rm -rf /usr/local/tinygo && tar -C /usr/local -xzf tinygo.tgz && rm -rf tinygo.tgz; \
|
||||
echo "oras_url: '$oras_url'"; \
|
||||
wget -O oras.tgz "$oras_url" --progress=dot:giga; \
|
||||
tar -C /usr/local/bin -xzf oras.tgz && rm -rf oras.tgz; \
|
||||
echo "go_url: $go_url"; \
|
||||
wget -O go.tgz "$go_url" --progress=dot:giga || (echo "Failed to download Go" && exit 1); \
|
||||
tar -C /usr/local -xzf go.tgz && rm go.tgz; \
|
||||
echo "oras_url: $oras_url"; \
|
||||
wget -O oras.tgz "$oras_url" --progress=dot:giga || (echo "Failed to download ORAS" && exit 1); \
|
||||
tar -C /usr/local/bin -xzf oras.tgz && rm oras.tgz; \
|
||||
echo "done";
|
||||
|
||||
ENV PATH=$PATH:/usr/local/go/bin:/usr/local/tinygo/bin:/usr/local/bin
|
||||
ENV PATH=$PATH:/usr/local/go/bin:/usr/local/bin
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user