mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 21:21:01 +08:00
Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f47d3fc12 | ||
|
|
6773482300 | ||
|
|
b6d61f9568 | ||
|
|
1834d4acef | ||
|
|
7f9ae38e51 | ||
|
|
b13bce6a36 | ||
|
|
275cac9dbb | ||
|
|
8cce7f5d50 | ||
|
|
4f0834d817 | ||
|
|
7cf0dae824 | ||
|
|
707061fb68 | ||
|
|
3255925bf0 | ||
|
|
a44f7ef76e | ||
|
|
c7abfb8aff | ||
|
|
ed925ddf84 | ||
|
|
1301af4638 | ||
|
|
de6144439f | ||
|
|
e37c4dc286 | ||
|
|
b8e0baa5ab | ||
|
|
4a157e98e9 | ||
|
|
6af8b17216 | ||
|
|
4500b10a42 | ||
|
|
c5a86b5298 | ||
|
|
36806d9e5c | ||
|
|
d1700009e8 | ||
|
|
2c3188dad7 | ||
|
|
7d423cddbd | ||
|
|
0e94e1a58a | ||
|
|
b1307ba97e | ||
|
|
8ae810b01a | ||
|
|
83b38b896c | ||
|
|
1385028f01 | ||
|
|
af663b701a | ||
|
|
e5c24a10fb | ||
|
|
ea85ccb694 | ||
|
|
2467004dc9 | ||
|
|
5af818a94e | ||
|
|
728a9de165 | ||
|
|
823527ab94 | ||
|
|
cb7f6ccd0f | ||
|
|
5107ce5137 | ||
|
|
e6d32aa1cf | ||
|
|
3c73976130 | ||
|
|
639956c0b8 | ||
|
|
a602f7a725 | ||
|
|
7b6e4154f4 | ||
|
|
12e3f34c0b | ||
|
|
bdd802f44f | ||
|
|
d58b66df8f | ||
|
|
5d99c7d80a | ||
|
|
3428932aca | ||
|
|
7ba3f75d41 | ||
|
|
ae9a06b05c | ||
|
|
9ebe968921 | ||
|
|
93e3b086ce | ||
|
|
20dfc3d64f | ||
|
|
492c5d350a | ||
|
|
037c71a320 | ||
|
|
9a07c50f44 | ||
|
|
b86e9fc938 | ||
|
|
2014234356 | ||
|
|
83f69a0186 | ||
|
|
8495d17070 | ||
|
|
6f762b5e4c | ||
|
|
96e4713703 | ||
|
|
d3887835a3 | ||
|
|
1965d107d0 | ||
|
|
b2f9bf94fa | ||
|
|
9257077fa3 | ||
|
|
7e310a3520 | ||
|
|
663b28fa9b | ||
|
|
9fbe331f5f | ||
|
|
dd50ac09dc | ||
|
|
8450a0869b | ||
|
|
bd6708552d | ||
|
|
50cfa0bb4b | ||
|
|
ea0143829d | ||
|
|
f83e66c23b | ||
|
|
87fe1aeeb5 | ||
|
|
386a208b14 | ||
|
|
ee77ffb753 | ||
|
|
6eeef07621 | ||
|
|
8978a4e0e0 | ||
|
|
71029d791d | ||
|
|
d9f16f7d5e | ||
|
|
f5d20b72e0 | ||
|
|
9bde0dfb46 | ||
|
|
f5c1e7f2ec | ||
|
|
45fbc8b084 | ||
|
|
1812a6b0a9 | ||
|
|
2640c76760 | ||
|
|
4223b2d666 | ||
|
|
dee4786c1c | ||
|
|
e549c79ae4 | ||
|
|
6742df57df | ||
|
|
eef8adf42f | ||
|
|
029c3e75fc | ||
|
|
9fa3a730d5 | ||
|
|
9acaed0b43 | ||
|
|
f95264448c | ||
|
|
e0dc9672ac | ||
|
|
5de7c2a5ea | ||
|
|
9a89665b22 | ||
|
|
4a82d50d80 | ||
|
|
34b3fc3114 | ||
|
|
f09e029a6b | ||
|
|
5e7e20ff7e | ||
|
|
26bfdd45ff | ||
|
|
61defc13c6 | ||
|
|
19496e5759 | ||
|
|
beb60fcacd | ||
|
|
01cc7939ae | ||
|
|
5a5af4ecbf | ||
|
|
d172cf4d19 | ||
|
|
58c4ba2021 | ||
|
|
9e2df8f7c7 | ||
|
|
b897825069 | ||
|
|
f45bc9008a | ||
|
|
5536502c15 | ||
|
|
a0c334a7cb | ||
|
|
9e6bd6d2cc | ||
|
|
ab419efda4 | ||
|
|
d4155411ee | ||
|
|
d721c235cb | ||
|
|
0905cd0fc0 | ||
|
|
188914a16b | ||
|
|
988e2c1fa7 | ||
|
|
4f1901586a | ||
|
|
80b58e86e1 | ||
|
|
ca32e587d3 | ||
|
|
6d2d98f653 | ||
|
|
2d1d8ac2b1 | ||
|
|
a2b8f9a646 | ||
|
|
5bece9c8ef | ||
|
|
45fdd95a9c | ||
|
|
d3afe345ad | ||
|
|
90ca903d2e | ||
|
|
2d8a8f26da | ||
|
|
9ea2410388 | ||
|
|
9e1792c245 | ||
|
|
3eda7def89 | ||
|
|
1787553294 | ||
|
|
f6c48415d1 | ||
|
|
e27d3d0971 | ||
|
|
49617c7a98 | ||
|
|
53a015d8fe | ||
|
|
e711e9f997 | ||
|
|
8530742472 | ||
|
|
c0c1f5113a | ||
|
|
2e6ddd7e35 | ||
|
|
2328e19c9d | ||
|
|
fabc22f218 | ||
|
|
2986e1911d | ||
|
|
a566f7257d |
@@ -133,6 +133,11 @@ jobs:
|
||||
command="
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-rust/extensions/${PLUGIN_NAME}
|
||||
if [ -f ./.prebuild ]; then
|
||||
echo 'Found .prebuild file, sourcing it...'
|
||||
. ./.prebuild
|
||||
fi
|
||||
rustup target add wasm32-wasip1
|
||||
cargo build --target wasm32-wasip1 --release
|
||||
cp target/wasm32-wasip1/release/*.wasm plugin.wasm
|
||||
tar czvf plugin.tar.gz plugin.wasm
|
||||
|
||||
4
.github/workflows/build-and-test-plugin.yaml
vendored
4
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
# There are too many lint errors in current code bases
|
||||
# uncomment when we decide what lint should be addressed or ignored.
|
||||
# - run: make lint
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
||||
8
.github/workflows/build-and-test.yaml
vendored
8
.github/workflows/build-and-test.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
# There are too many lint errors in current code bases
|
||||
# uncomment when we decide what lint should be addressed or ignored.
|
||||
# - run: make lint
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
|
||||
487
.github/workflows/build-image-and-push.yaml
vendored
487
.github/workflows/build-image-and-push.yaml
vendored
@@ -1,229 +1,258 @@
|
||||
name: Build Docker Images and Push to Image Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
build-controller-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-controller
|
||||
env:
|
||||
CONTROLLER_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Docker Image and Push
|
||||
run: |
|
||||
GOPROXY="https://proxy.golang.org,direct" make docker-buildx-push
|
||||
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress"
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
|
||||
done
|
||||
|
||||
build-pilot-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-pilot
|
||||
env:
|
||||
PILOT_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Pilot-Discovery Image and Push
|
||||
run: |
|
||||
GOPROXY="https://proxy.golang.org,direct" make build-istio
|
||||
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot"
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
|
||||
done
|
||||
|
||||
|
||||
build-gateway-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-pilot
|
||||
env:
|
||||
GATEWAY_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Gateway Image and Push
|
||||
run: |
|
||||
GOPROXY="https://proxy.golang.org,direct" make build-gateway
|
||||
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2"
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
|
||||
done
|
||||
name: Build Docker Images and Push to Image Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
build-controller-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-controller
|
||||
env:
|
||||
CONTROLLER_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Docker Image and Push
|
||||
run: |
|
||||
BUILT_IMAGE=""
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
if [ "$BUILT_IMAGE" == "" ]; then
|
||||
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$image" make docker-buildx-push
|
||||
BUILT_IMAGE="$image"
|
||||
else
|
||||
docker buildx imagetools create $BUILT_IMAGE --tag $image
|
||||
fi
|
||||
done
|
||||
|
||||
build-pilot-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-pilot
|
||||
env:
|
||||
PILOT_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Pilot-Discovery Image and Push
|
||||
run: |
|
||||
BUILT_IMAGE=""
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
if [ "$BUILT_IMAGE" == "" ]; then
|
||||
TAG=${image#*:}
|
||||
HUB=${image%:*}
|
||||
HUB=${HUB%/*}
|
||||
BUILT_IMAGE="$HUB/pilot:$TAG"
|
||||
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$BUILT_IMAGE" make build-istio
|
||||
fi
|
||||
if [ "$BUILT_IMAGE" != "$image" ]; then
|
||||
docker buildx imagetools create $BUILT_IMAGE --tag $image
|
||||
fi
|
||||
done
|
||||
|
||||
build-gateway-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-gateway
|
||||
env:
|
||||
GATEWAY_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Gateway Image and Push
|
||||
run: |
|
||||
BUILT_IMAGE=""
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
if [ "$BUILT_IMAGE" == "" ]; then
|
||||
TAG=${image#*:}
|
||||
HUB=${image%:*}
|
||||
HUB=${HUB%/*}
|
||||
BUILT_IMAGE="$HUB/proxyv2:$TAG"
|
||||
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$BUILT_IMAGE" make build-gateway
|
||||
fi
|
||||
if [ "$BUILT_IMAGE" != "$image" ]; then
|
||||
docker buildx imagetools create $BUILT_IMAGE --tag $image
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -20,11 +20,11 @@ jobs:
|
||||
name: Prepare Standalone Package
|
||||
run: |
|
||||
mkdir ./artifact
|
||||
cp ./tools/get-higress.sh ./artifact
|
||||
LOCAL_RELEASE_URL="https://github.com/higress-group/higress-standalone/releases"
|
||||
VERSION=$(curl -Ls $LOCAL_RELEASE_URL | grep 'href="/higress-group/higress-standalone/releases/tag/v[0-9]*.[0-9]*.[0-9]*\"' | sed -E 's/.*\/higress-group\/higress-standalone\/releases\/tag\/(v[0-9\.]+)".*/\1/g' | head -1)
|
||||
DOWNLOAD_URL="https://github.com/higress-group/higress-standalone/archive/refs/tags/${VERSION}.tar.gz"
|
||||
curl -SsL "$DOWNLOAD_URL" -o "./artifact/higress-${VERSION}.tar.gz"
|
||||
curl -SsL "https://raw.githubusercontent.com/higress-group/higress-standalone/refs/heads/main/src/get-higress.sh" -o "./artifact/get-higress.sh"
|
||||
echo -n "$VERSION" > ./artifact/VERSION
|
||||
echo "Version=$VERSION"
|
||||
# Step 3
|
||||
|
||||
70
.github/workflows/helm-docs.yaml
vendored
70
.github/workflows/helm-docs.yaml
vendored
@@ -6,11 +6,11 @@ on:
|
||||
- "*"
|
||||
paths:
|
||||
- 'helm/**'
|
||||
workflow_dispatch: ~
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- 'helm/**'
|
||||
|
||||
jobs:
|
||||
helm:
|
||||
@@ -51,7 +51,26 @@ jobs:
|
||||
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 }}
|
||||
@@ -78,37 +97,30 @@ jobs:
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
echo "response: $RESPONSE"
|
||||
echo "Response: $RESPONSE"
|
||||
|
||||
TRANSLATED_CONTENT=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
|
||||
echo "$RESPONSE" | jq -c -r '.choices[] | .message.content' > README.zh.new.md
|
||||
|
||||
if [ -z "$TRANSLATED_CONTENT" ]; then
|
||||
echo "Translation failed! Response: $RESPONSE"
|
||||
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
|
||||
|
||||
echo "$TRANSLATED_CONTENT" > README.zh.new.md
|
||||
echo "Translation completed and saved to README.zh.new.md."
|
||||
mv README.zh.new.md README.zh.md
|
||||
|
||||
- name: Compare README.zh.md
|
||||
id: compare
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
NEW_README_ZH="README.zh.new.md"
|
||||
EXISTING_README_ZH="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.
|
||||
|
||||
if [ ! -f "$EXISTING_README_ZH" ]; then
|
||||
echo "Add README.zh.md."
|
||||
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
|
||||
echo "updated=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! diff -q "$NEW_README_ZH" "$EXISTING_README_ZH"; then
|
||||
echo "Files are different. Updating README.zh.md."
|
||||
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
|
||||
echo "updated=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Files are identical. No update needed."
|
||||
echo "updated=false" >> $GITHUB_ENV
|
||||
fi
|
||||
- Automatically generated by GitHub Actions
|
||||
labels: translation, automated
|
||||
base: main
|
||||
6
.github/workflows/release-hgctl.yaml
vendored
6
.github/workflows/release-hgctl.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Build hgctl latest multiarch binaries
|
||||
run: |
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Build hgctl latest macos binaries
|
||||
run: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
go-version: 1.22
|
||||
|
||||
- name: Build hgctl latest macos binaries
|
||||
run: |
|
||||
|
||||
@@ -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.1/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.4/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -159,16 +159,20 @@ build-pilot-local: prebuild
|
||||
buildx-prepare:
|
||||
docker buildx inspect multi-arch >/dev/null 2>&1 || docker buildx create --name multi-arch --platform linux/amd64,linux/arm64 --use
|
||||
|
||||
build-gateway: prebuild buildx-prepare
|
||||
build-gateway: prebuild buildx-prepare build-golang-filter
|
||||
USE_REAL_USER=1 TARGET_ARCH=amd64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
|
||||
USE_REAL_USER=1 TARGET_ARCH=arm64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
|
||||
DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
DOCKER_TARGETS="docker.proxyv2" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
|
||||
build-gateway-local: prebuild
|
||||
build-gateway-local: prebuild build-golang-filter
|
||||
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker
|
||||
|
||||
build-golang-filter:
|
||||
TARGET_ARCH=amd64 ./tools/hack/build-golang-filters.sh
|
||||
TARGET_ARCH=arm64 ./tools/hack/build-golang-filters.sh
|
||||
|
||||
build-istio: prebuild buildx-prepare
|
||||
DOCKER_TARGETS="docker.pilot" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
DOCKER_TARGETS="docker.pilot" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
|
||||
build-istio-local: prebuild
|
||||
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.pilot" ./tools/hack/build-istio-image.sh docker
|
||||
@@ -231,6 +235,7 @@ clean-gateway: clean-istio
|
||||
rm -rf external/proxy
|
||||
rm -rf external/go-control-plane
|
||||
rm -rf external/package/envoy.tar.gz
|
||||
rm -rf external/package/*.so
|
||||
|
||||
clean-env:
|
||||
rm -rf out/
|
||||
|
||||
205
README.md
205
README.md
@@ -14,192 +14,139 @@
|
||||
<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>
|
||||
</div>
|
||||
|
||||
[**官网**](https://higress.cn/) |
|
||||
[**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) |
|
||||
[**博客**](https://higress.cn/blog/) |
|
||||
[**电子书**](https://higress.cn/docs/ebook/wasm14/) |
|
||||
[**开发指引**](https://higress.cn/docs/latest/dev/architecture/) |
|
||||
[**AI插件**](https://higress.cn/plugin/)
|
||||
|
||||
|
||||
[**Official Site**](https://higress.ai/en/) |
|
||||
[**MCP Server QuickStart**](https://higress.cn/en/ai/mcp-quick-start/) |
|
||||
[**Wasm Plugin Hub**](https://higress.cn/en/plugin/) |
|
||||
|
||||
<p>
|
||||
<a href="README_EN.md"> English <a/>| 中文 | <a href="README_JP.md"> 日本語 <a/>
|
||||
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
|
||||
</p>
|
||||
|
||||
Higress is a cloud-native API gateway based on Istio and Envoy, which can be extended with Wasm plugins written in Go/Rust/JS. It provides dozens of ready-to-use general-purpose plugins and an out-of-the-box console (try the [demo here](http://demo.higress.io/)).
|
||||
|
||||
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy,可以用 Go/Rust/JS 等编写 Wasm 插件,提供了数十个现成的通用插件,以及开箱即用的控制台(demo 点[这里](http://demo.higress.io/))
|
||||
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo.
|
||||
|
||||
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
|
||||
|
||||
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
|
||||
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型;在阿里云内部支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
|
||||
|
||||

|
||||
Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
|
||||
|
||||
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international, as well as self-built DeepSeek models based on vllm/ollama. Within Alibaba Cloud, it supports AI businesses such as Tongyi Qianwen APP, Bailian large model API, and machine learning PAI platform. It also serves leading AIGC enterprises (such as Zero One Infinite) and AI products (such as FastGPT).
|
||||
|
||||
## Summary
|
||||
|
||||
- [**快速开始**](#快速开始)
|
||||
- [**功能展示**](#功能展示)
|
||||
- [**使用场景**](#使用场景)
|
||||
- [**核心优势**](#核心优势)
|
||||
- [**社区**](#社区)
|
||||
- [**Quick Start**](#quick-start)
|
||||
- [**Feature Showcase**](#feature-showcase)
|
||||
- [**Use Cases**](#use-cases)
|
||||
- [**Core Advantages**](#core-advantages)
|
||||
- [**Community**](#community)
|
||||
|
||||
## 快速开始
|
||||
## Quick Start
|
||||
|
||||
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
|
||||
Higress can be started with just Docker, making it convenient for individual developers to set up locally for learning or for building simple sites:
|
||||
|
||||
```bash
|
||||
# 创建一个工作目录
|
||||
# Create a working directory
|
||||
mkdir higress; cd higress
|
||||
# 启动 higress,配置文件会写到工作目录下
|
||||
# Start higress, configuration files will be written to the working directory
|
||||
docker run -d --rm --name higress-ai -v ${PWD}:/data \
|
||||
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
|
||||
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
|
||||
```
|
||||
|
||||
监听端口说明如下:
|
||||
Port descriptions:
|
||||
|
||||
- 8001 端口:Higress UI 控制台入口
|
||||
- 8080 端口:网关 HTTP 协议入口
|
||||
- 8443 端口:网关 HTTPS 协议入口
|
||||
- Port 8001: Higress UI console entry
|
||||
- Port 8080: Gateway HTTP protocol entry
|
||||
- Port 8443: Gateway HTTPS protocol entry
|
||||
|
||||
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
|
||||
> 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.
|
||||
|
||||
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
|
||||
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).
|
||||
|
||||
如果您是在云上部署,生产环境推荐使用[企业版](https://higress.io/cloud/),开发测试可以使用下面一键部署社区版:
|
||||
## Use Cases
|
||||
|
||||
[](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Higress社区版)
|
||||
- **AI Gateway**:
|
||||
|
||||
Higress can connect to all LLM model providers both domestic and international using a unified protocol, while also providing rich AI observability, multi-model load balancing/fallback, AI token rate limiting, AI caching, and other capabilities:
|
||||
|
||||
## 使用场景
|
||||

|
||||
|
||||
- **AI 网关**:
|
||||
- **MCP Server Hosting**:
|
||||
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
Higress, as an Envoy-based API gateway, supports hosting MCP Servers through its plugin mechanism. MCP (Model Context Protocol) is essentially an AI-friendly API that enables AI Agents to more easily call various tools and services. Higress provides unified capabilities for authentication, authorization, rate limiting, and observability for tool calls, simplifying the development and deployment of AI applications.
|
||||
|
||||

|
||||

|
||||
|
||||
- **Kubernetes Ingress 网关**:
|
||||
|
||||
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
|
||||
**🌟 Try it now!** Experience Higress-hosted Remote MCP Servers at [https://mcp.higress.ai/](https://mcp.higress.ai/)
|
||||
|
||||
支持 [Gateway API](https://gateway-api.sigs.k8s.io/) 标准,支持用户从 Ingress API 平滑迁移到 Gateway API。
|
||||

|
||||
|
||||
相比 ingress-nginx,资源开销大幅下降,路由变更生效速度有十倍提升:
|
||||
By hosting MCP Servers with Higress, you can achieve:
|
||||
- Unified authentication and authorization mechanisms, ensuring the security of AI tool calls
|
||||
- Fine-grained rate limiting to prevent abuse and resource exhaustion
|
||||
- Comprehensive audit logs recording all tool call behaviors
|
||||
- Rich observability for monitoring the performance and health of tool calls
|
||||
- Simplified deployment and management through Higress's plugin mechanism for quickly adding new MCP Servers
|
||||
- Dynamic updates without disruption: Thanks to Envoy's friendly handling of long connections and Wasm plugin's dynamic update mechanism, MCP Server logic can be updated on-the-fly without any traffic disruption or connection drops
|
||||
|
||||

|
||||

|
||||
[Learn more...](https://higress.cn/en/ai/mcp-quick-start/?spm=36971b57.7beea2de.0.0.d85f20a94jsWGm)
|
||||
|
||||
- **Kubernetes ingress controller**:
|
||||
|
||||
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
|
||||
|
||||
- **微服务网关**:
|
||||
|
||||
Higress 可以作为微服务网关, 能够对接多种类型的注册中心发现服务配置路由,例如 Nacos, ZooKeeper, Consul, Eureka 等。
|
||||
[Gateway API](https://gateway-api.sigs.k8s.io/) support is coming soon and will support smooth migration from Ingress API to Gateway API.
|
||||
|
||||
并且深度集成了 [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) 等微服务技术栈,基于 Envoy C++ 网关内核的出色性能,相比传统 Java 类微服务网关,可以显著降低资源使用率,减少成本。
|
||||
- **Microservice gateway**:
|
||||
|
||||

|
||||
Higress can function as a microservice gateway, which can discovery microservices from various service registries, such as Nacos, ZooKeeper, Consul, Eureka, etc.
|
||||
|
||||
- **安全防护网关**:
|
||||
It deeply integrates with [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
|
||||
|
||||
- **Security gateway**:
|
||||
|
||||
Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
|
||||
Higress can be used as a security gateway, supporting WAF and various authentication strategies, such as key-auth, hmac-auth, jwt-auth, basic-auth, oidc, etc.
|
||||
|
||||
## 核心优势
|
||||
|
||||
- **生产等级**
|
||||
## Core Advantages
|
||||
|
||||
脱胎于阿里巴巴2年多生产验证的内部产品,支持每秒请求量达数十万级的大规模场景。
|
||||
- **Production Grade**
|
||||
|
||||
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
|
||||
Born from Alibaba's internal product with over 2 years of production validation, supporting large-scale scenarios with hundreds of thousands of requests per second.
|
||||
|
||||
- **流式处理**
|
||||
Completely eliminates traffic jitter caused by Nginx reload, configuration changes take effect in milliseconds and are transparent to business. Especially friendly to long-connection scenarios such as AI businesses.
|
||||
|
||||
支持真正的完全流式处理请求/响应 Body,Wasm 插件很方便地自定义处理 SSE (Server-Sent Events)等流式协议的报文。
|
||||
- **Streaming Processing**
|
||||
|
||||
在 AI 业务等大带宽场景下,可以显著降低内存开销。
|
||||
Supports true complete streaming processing of request/response bodies, Wasm plugins can easily customize the handling of streaming protocols such as SSE (Server-Sent Events).
|
||||
|
||||
In high-bandwidth scenarios such as AI businesses, it can significantly reduce memory overhead.
|
||||
|
||||
- **便于扩展**
|
||||
- **Easy to Extend**
|
||||
|
||||
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能,满足90%以上的业务场景需求。
|
||||
Provides a rich official plugin library covering AI, traffic management, security protection and other common functions, meeting more than 90% of business scenario requirements.
|
||||
|
||||
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
|
||||
Focuses on Wasm plugin extensions, ensuring memory safety through sandbox isolation, supporting multiple programming languages, allowing plugin versions to be upgraded independently, and achieving traffic-lossless hot updates of gateway logic.
|
||||
|
||||
- **安全易用**
|
||||
- **Secure and Easy to Use**
|
||||
|
||||
基于 Ingress API 和 Gateway API 标准,提供开箱即用的 UI 控制台,WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
|
||||
Based on Ingress API and Gateway API standards, provides out-of-the-box UI console, WAF protection plugin, IP/Cookie CC protection plugin ready to use.
|
||||
|
||||
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
|
||||
Supports connecting to Let's Encrypt for automatic issuance and renewal of free certificates, and can be deployed outside of K8s, started with a single Docker command, convenient for individual developers to use.
|
||||
|
||||
## Community
|
||||
|
||||
## 功能展示
|
||||
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
|
||||
|
||||
### AI 网关 Demo 展示
|
||||
### Thanks
|
||||
|
||||
[从 OpenAI 到其他大模型,30 秒完成迁移
|
||||
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
|
||||
Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank you to Envoy and Istio.
|
||||
|
||||
### Related Repositories
|
||||
|
||||
### Higress UI 控制台
|
||||
|
||||
- **丰富的可观测**
|
||||
- Higress Console: https://github.com/higress-group/higress-console
|
||||
- Higress Standalone: https://github.com/higress-group/higress-standalone
|
||||
|
||||
提供开箱即用的可观测,Grafana&Prometheus 可以使用内置的也可对接自建的
|
||||
|
||||

|
||||
|
||||
|
||||
- **插件扩展机制**
|
||||
|
||||
官方提供了多种插件,用户也可以[开发](./plugins/wasm-go)自己的插件,构建成 docker/oci 镜像后在控制台配置,可以实时变更插件逻辑,对流量完全无损。
|
||||
|
||||

|
||||
|
||||
|
||||
- **多种服务发现**
|
||||
|
||||
默认提供 K8s Service 服务发现,通过配置可以对接 Nacos/ZooKeeper 等注册中心实现服务发现,也可以基于静态 IP 或者 DNS 来发现
|
||||
|
||||

|
||||
|
||||
|
||||
- **域名和证书**
|
||||
|
||||
可以创建管理 TLS 证书,并配置域名的 HTTP/HTTPS 行为,域名策略里支持对特定域名生效插件
|
||||
|
||||

|
||||
|
||||
|
||||
- **丰富的路由能力**
|
||||
|
||||
通过上面定义的服务发现机制,发现的服务会出现在服务列表中;创建路由时,选择域名,定义路由匹配机制,再选择目标服务进行路由;路由策略里支持对特定路由生效插件
|
||||
|
||||

|
||||
|
||||
|
||||
## 社区
|
||||
|
||||
### 感谢
|
||||
|
||||
如果没有 Envoy 和 Istio 的开源工作,Higress 就不可能实现,在这里向这两个项目献上最诚挚的敬意。
|
||||
|
||||
### 交流群
|
||||
|
||||

|
||||
|
||||
### 技术分享
|
||||
|
||||
微信公众号:
|
||||
|
||||

|
||||
|
||||
### 关联仓库
|
||||
|
||||
- Higress 控制台:https://github.com/higress-group/higress-console
|
||||
- Higress(独立运行版):https://github.com/higress-group/higress-standalone
|
||||
|
||||
### 贡献者
|
||||
### Contributors
|
||||
|
||||
<a href="https://github.com/alibaba/higress/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
|
||||
@@ -207,10 +154,10 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||
### Star History
|
||||
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ 返回顶部 ↑
|
||||
↑ Back to Top ↑
|
||||
</a>
|
||||
</p>
|
||||
|
||||
106
README_EN.md
106
README_EN.md
@@ -1,106 +0,0 @@
|
||||
<a name="readme-top"></a>
|
||||
<h1 align="center">
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
|
||||
<br>
|
||||
Cloud Native API Gateway
|
||||
</h1>
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
[**Official Site**](https://higress.io/en-us/) |
|
||||
[**Docs**](https://higress.io/en-us/docs/overview/what-is-higress) |
|
||||
[**Blog**](https://higress.io/en-us/blog) |
|
||||
[**Developer**](https://higress.io/en-us/docs/developers/developers_dev) |
|
||||
[**Higress in Cloud**](https://www.alibabacloud.com/product/microservices-engine?spm=higress-website.topbar.0.0.0)
|
||||
|
||||
|
||||
<p>
|
||||
English | <a href="README.md">中文<a/> | <a href="README_JP.md">日本語<a/>
|
||||
</p>
|
||||
|
||||
Higress is a cloud-native api gateway based on Alibaba's internal gateway practices.
|
||||
|
||||
Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.com/envoyproxy/envoy), Higress realizes the integration of the triple gateway architecture of traffic gateway, microservice gateway and security gateway, thereby greatly reducing the costs of deployment, operation and maintenance.
|
||||
|
||||
<h1 align="center">
|
||||
<img src="https://img.alicdn.com/imgextra/i1/O1CN01iO9ph825juHbOIg75_!!6000000007563-2-tps-2483-2024.png" alt="Higress Architecture">
|
||||
</h1>
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
- [**Use Cases**](#use-cases)
|
||||
- [**Higress Features**](#higress-features)
|
||||
- [**Quick Start**](https://higress.io/en-us/docs/user/quickstart)
|
||||
- [**Community**](#community)
|
||||
- [**Thanks**](#thanks)
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Kubernetes ingress controller**:
|
||||
|
||||
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
|
||||
|
||||
[Gateway API](https://gateway-api.sigs.k8s.io/) support is coming soon and will support smooth migration from Ingress API to Gateway API.
|
||||
|
||||
- **Microservice gateway**:
|
||||
|
||||
Higress can function as a microservice gateway, which can discovery microservices from various service registries, such as Nacos, ZooKeeper, Consul, Eureka, etc.
|
||||
|
||||
It deeply integrates with [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
|
||||
|
||||
- **Security gateway**:
|
||||
|
||||
Higress can be used as a security gateway, supporting WAF and various authentication strategies, such as key-auth, hmac-auth, jwt-auth, basic-auth, oidc, etc.
|
||||
|
||||
## Higress Features
|
||||
|
||||
- **Easy to use**
|
||||
|
||||
Provides one-stop gateway solutions for traffic scheduling, service management, and security protection, support Console, K8s Ingress, and Gateway API configuration methods, and also support HTTP to Dubbo protocol conversion, and easily complete protocol mapping configuration.
|
||||
|
||||
- **Easy to expand**
|
||||
|
||||
Provides Wasm, Lua, and out-of-process plug-in extension mechanisms, so that multi-language plug-in writing is no longer an obstacle. The granularity of plug-in effectiveness supports not only the global level, domain name level, but also fine-grained routing level
|
||||
|
||||
- **Dynamic hot update**
|
||||
|
||||
Get rid of the traffic jitter caused by reload at the bottom, the configuration change takes effect in milliseconds and the business is not affected, the Wasm plug-in is hot updated and the traffic is not damaged
|
||||
|
||||
- **Smooth upgrade**
|
||||
|
||||
Compatible with 80%+ usage scenarios of Nginx Ingress Annotation, and provides more feature-rich annotations, easy to handle Nginx Ingress migration in one step
|
||||
|
||||
- **Security**
|
||||
|
||||
Provides JWT, OIDC, custom authentication and authentication, deeply integrates open-source web application firewall.
|
||||
|
||||
## Community
|
||||
|
||||
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
|
||||
|
||||
### Thanks
|
||||
|
||||
Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank you to Envoy and Istio.
|
||||
|
||||
### Related Repositories
|
||||
|
||||
- Higress Console: https://github.com/higress-group/higress-console
|
||||
- Higress Standalone: https://github.com/higress-group/higress-standalone
|
||||
|
||||
### Contributors
|
||||
|
||||
<a href="https://github.com/alibaba/higress/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
|
||||
</a>
|
||||
|
||||
### Star History
|
||||
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ Back to Top ↑
|
||||
</a>
|
||||
</p>
|
||||
20
README_JP.md
20
README_JP.md
@@ -73,6 +73,24 @@ K8sでのHelmデプロイなどの他のインストール方法については
|
||||
|
||||

|
||||
|
||||
- **MCP Server ホスティング**:
|
||||
|
||||
Higressは、EnvoyベースのAPIゲートウェイとして、プラグインメカニズムを通じてMCP Serverをホストすることができます。MCP(Model Context Protocol)は本質的にAIにより親和性の高いAPIであり、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。Higressはツール呼び出しの認証、認可、レート制限、可観測性などの統一機能を提供し、AIアプリケーションの開発とデプロイを簡素化します。
|
||||
|
||||

|
||||
|
||||
**🌟 今すぐ試してみよう!** [https://mcp.higress.ai/](https://mcp.higress.ai/) でHigressがホストするリモートMCPサーバーを体験できます。このプラットフォームでは、HigressがどのようにリモートMCPサーバーをホストおよび管理するかを直接体験できます。
|
||||
|
||||

|
||||
|
||||
Higressを使用してMCP Serverをホストすることで、以下のことが実現できます:
|
||||
- 統一された認証と認可メカニズム、AIツール呼び出しのセキュリティを確保
|
||||
- きめ細かいレート制限、乱用やリソース枯渇を防止
|
||||
- 包括的な監査ログ、すべてのツール呼び出し行動を記録
|
||||
- 豊富な可観測性、ツール呼び出しのパフォーマンスと健全性を監視
|
||||
- 簡素化されたデプロイと管理、Higressのプラグインメカニズムを通じて新しいMCP Serverを迅速に追加
|
||||
- 動的更新による無停止:Envoyの長時間接続に対する友好的なサポートとWasmプラグインの動的更新メカニズムにより、MCP Serverのロジックをリアルタイムで更新でき、トラフィックに完全に影響を与えず、接続が切断されることはありません
|
||||
|
||||
- **Kubernetes Ingressゲートウェイ**:
|
||||
|
||||
HigressはK8sクラスターのIngressエントリーポイントゲートウェイとして機能し、多くのK8s Nginx Ingressの注釈に対応しています。K8s Nginx IngressからHigressへのスムーズな移行が可能です。
|
||||
@@ -203,4 +221,4 @@ WeChat公式アカウント:
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ トップに戻る ↑
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
234
README_ZH.md
Normal file
234
README_ZH.md
Normal file
@@ -0,0 +1,234 @@
|
||||
<a name="readme-top"></a>
|
||||
<h1 align="center">
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
|
||||
<br>
|
||||
AI Gateway
|
||||
</h1>
|
||||
<h4 align="center"> AI Native API Gateway </h4>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
[**官网**](https://higress.cn/) |
|
||||
[**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) |
|
||||
[**博客**](https://higress.cn/blog/) |
|
||||
[**电子书**](https://higress.cn/docs/ebook/wasm14/) |
|
||||
[**开发指引**](https://higress.cn/docs/latest/dev/architecture/) |
|
||||
[**AI插件**](https://higress.cn/plugin/)
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
<a href="README.md"> English <a/>| 中文 | <a href="README_JP.md"> 日本語 <a/>
|
||||
</p>
|
||||
|
||||
|
||||
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy,可以用 Go/Rust/JS 等编写 Wasm 插件,提供了数十个现成的通用插件,以及开箱即用的控制台(demo 点[这里](http://demo.higress.io/))
|
||||
|
||||
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
|
||||
|
||||
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
|
||||
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型;在阿里云内部支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
|
||||
|
||||

|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
- [**快速开始**](#快速开始)
|
||||
- [**功能展示**](#功能展示)
|
||||
- [**使用场景**](#使用场景)
|
||||
- [**核心优势**](#核心优势)
|
||||
- [**社区**](#社区)
|
||||
|
||||
## 快速开始
|
||||
|
||||
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
|
||||
|
||||
```bash
|
||||
# 创建一个工作目录
|
||||
mkdir higress; cd higress
|
||||
# 启动 higress,配置文件会写到工作目录下
|
||||
docker run -d --rm --name higress-ai -v ${PWD}:/data \
|
||||
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
|
||||
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
|
||||
```
|
||||
|
||||
监听端口说明如下:
|
||||
|
||||
- 8001 端口:Higress UI 控制台入口
|
||||
- 8080 端口:网关 HTTP 协议入口
|
||||
- 8443 端口:网关 HTTPS 协议入口
|
||||
|
||||
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
|
||||
|
||||
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
|
||||
|
||||
如果您是在云上部署,生产环境推荐使用[企业版](https://higress.io/cloud/),开发测试可以使用下面一键部署社区版:
|
||||
|
||||
[](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Higress社区版)
|
||||
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **AI 网关**:
|
||||
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
|
||||

|
||||
|
||||
- **MCP Server 托管**:
|
||||
|
||||
Higress 作为基于 Envoy 的 API 网关,支持通过插件方式托管 MCP Server。MCP(Model Context Protocol)本质是面向 AI 更友好的 API,使 AI Agent 能够更容易地调用各种工具和服务。Higress 可以统一处理工具调用的认证/鉴权/限流/观测等能力,简化 AI 应用的开发和部署。
|
||||
|
||||

|
||||
|
||||
**🌟 立即体验!** 在 [https://mcp.higress.ai/](https://mcp.higress.ai/) 体验 Higress 托管的远程 MCP 服务器。这个平台让您可以体验基于 Higress 托管的远程 MCP 服务器的效果。
|
||||
|
||||

|
||||
|
||||
通过 Higress 托管 MCP Server,可以实现:
|
||||
- 统一的认证和鉴权机制,确保 AI 工具调用的安全性
|
||||
- 精细化的速率限制,防止滥用和资源耗尽
|
||||
- 完整的审计日志,记录所有工具调用行为
|
||||
- 丰富的可观测性,监控工具调用的性能和健康状况
|
||||
- 简化的部署和管理,通过 Higress 插件机制快速添加新的 MCP Server
|
||||
- 动态更新无损:得益于 Envoy 对长连接保持的友好支持,以及 Wasm 插件的动态更新机制,MCP Server 逻辑可以实时更新,且对流量完全无损,不会导致任何连接断开
|
||||
|
||||
- **Kubernetes Ingress 网关**:
|
||||
|
||||
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
|
||||
|
||||
支持 [Gateway API](https://gateway-api.sigs.k8s.io/) 标准,支持用户从 Ingress API 平滑迁移到 Gateway API。
|
||||
|
||||
相比 ingress-nginx,资源开销大幅下降,路由变更生效速度有十倍提升:
|
||||
|
||||

|
||||

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

|
||||
|
||||
- **安全防护网关**:
|
||||
|
||||
Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
|
||||
|
||||
## 核心优势
|
||||
|
||||
- **生产等级**
|
||||
|
||||
脱胎于阿里巴巴2年多生产验证的内部产品,支持每秒请求量达数十万级的大规模场景。
|
||||
|
||||
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
|
||||
|
||||
- **流式处理**
|
||||
|
||||
支持真正的完全流式处理请求/响应 Body,Wasm 插件很方便地自定义处理 SSE (Server-Sent Events)等流式协议的报文。
|
||||
|
||||
在 AI 业务等大带宽场景下,可以显著降低内存开销。
|
||||
|
||||
- **便于扩展**
|
||||
|
||||
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能,满足90%以上的业务场景需求。
|
||||
|
||||
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
|
||||
|
||||
- **安全易用**
|
||||
|
||||
基于 Ingress API 和 Gateway API 标准,提供开箱即用的 UI 控制台,WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
|
||||
|
||||
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
|
||||
|
||||
|
||||
## 功能展示
|
||||
|
||||
### AI 网关 Demo 展示
|
||||
|
||||
[从 OpenAI 到其他大模型,30 秒完成迁移
|
||||
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
|
||||
|
||||
|
||||
### Higress UI 控制台
|
||||
|
||||
- **丰富的可观测**
|
||||
|
||||
提供开箱即用的可观测,Grafana&Prometheus 可以使用内置的也可对接自建的
|
||||
|
||||

|
||||
|
||||
|
||||
- **插件扩展机制**
|
||||
|
||||
官方提供了多种插件,用户也可以[开发](./plugins/wasm-go)自己的插件,构建成 docker/oci 镜像后在控制台配置,可以实时变更插件逻辑,对流量完全无损。
|
||||
|
||||

|
||||
|
||||
|
||||
- **多种服务发现**
|
||||
|
||||
默认提供 K8s Service 服务发现,通过配置可以对接 Nacos/ZooKeeper 等注册中心实现服务发现,也可以基于静态 IP 或者 DNS 来发现
|
||||
|
||||

|
||||
|
||||
|
||||
- **域名和证书**
|
||||
|
||||
可以创建管理 TLS 证书,并配置域名的 HTTP/HTTPS 行为,域名策略里支持对特定域名生效插件
|
||||
|
||||

|
||||
|
||||
|
||||
- **丰富的路由能力**
|
||||
|
||||
通过上面定义的服务发现机制,发现的服务会出现在服务列表中;创建路由时,选择域名,定义路由匹配机制,再选择目标服务进行路由;路由策略里支持对特定路由生效插件
|
||||
|
||||

|
||||
|
||||
|
||||
## 社区
|
||||
|
||||
### 感谢
|
||||
|
||||
如果没有 Envoy 和 Istio 的开源工作,Higress 就不可能实现,在这里向这两个项目献上最诚挚的敬意。
|
||||
|
||||
### 交流群
|
||||
|
||||

|
||||
|
||||
### 技术分享
|
||||
|
||||
微信公众号:
|
||||
|
||||

|
||||
|
||||
### 关联仓库
|
||||
|
||||
- Higress 控制台:https://github.com/higress-group/higress-console
|
||||
- Higress(独立运行版):https://github.com/higress-group/higress-standalone
|
||||
|
||||
### 贡献者
|
||||
|
||||
<a href="https://github.com/alibaba/higress/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
|
||||
</a>
|
||||
|
||||
### Star History
|
||||
|
||||
[](https://star-history.com/#alibaba/higress&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ 返回顶部 ↑
|
||||
</a>
|
||||
</p>
|
||||
@@ -35,6 +35,8 @@ DOCKER_ALL_VARIANTS ?= debug distroless
|
||||
INCLUDE_UNTAGGED_DEFAULT ?= false
|
||||
DEFAULT_DISTRIBUTION=debug
|
||||
|
||||
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(HUB)/higress:$(TAG)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
|
||||
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(HUB)/higress:$(TAG)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
|
||||
IMG ?= higress
|
||||
IMG_URL ?= $(HUB)/$(IMG):$(TAG)
|
||||
|
||||
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
|
||||
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
|
||||
|
||||
Submodule envoy/envoy updated: c2471f1247...e114a74dd8
6
go.mod
6
go.mod
@@ -1,8 +1,6 @@
|
||||
module github.com/alibaba/higress
|
||||
|
||||
go 1.21.0
|
||||
|
||||
toolchain go1.22.2
|
||||
go 1.22.2
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -23,6 +21,7 @@ require (
|
||||
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
|
||||
github.com/dubbogo/gost v1.13.1
|
||||
github.com/envoyproxy/go-control-plane v0.11.2-0.20230725211550-11bfe846bcd4
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
@@ -99,7 +98,6 @@ require (
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.7
|
||||
appVersion: 2.1.1
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
keywords:
|
||||
- higress
|
||||
- gateways
|
||||
- higress
|
||||
- gateways
|
||||
name: higress-core
|
||||
sources:
|
||||
- http://github.com/alibaba/higress
|
||||
- http://github.com/alibaba/higress
|
||||
dependencies:
|
||||
- condition: global.enableRedis
|
||||
name: redis
|
||||
repository: "file://../redis"
|
||||
version: 0.0.1
|
||||
type: application
|
||||
version: 2.0.7
|
||||
version: 2.1.1
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
Installs the core components of cloud-native gateway [Higress](http://higress.io/)
|
||||
|
||||
**Note:** It is highly recommended to install the whole package of Higress. Please visit https://higress.io/docs/user/quickstart/ for details.
|
||||
**Note:** It is highly recommended to install the whole package of Higress. Please visit https://higress.io/docs/user/quickstart/ for details.
|
||||
|
||||
23
helm/core/charts/redis/.helmignore
Normal file
23
helm/core/charts/redis/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
24
helm/core/charts/redis/Chart.yaml
Normal file
24
helm/core/charts/redis/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: redis
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.0.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "7.4.0-v3"
|
||||
34
helm/core/charts/redis/templates/_helpers.tpl
Normal file
34
helm/core/charts/redis/templates/_helpers.tpl
Normal file
@@ -0,0 +1,34 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
|
||||
{{- define "redis.name" -}}
|
||||
{{- .Values.redis.name | default "redis-stack-server" -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "redis.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "redis.labels" -}}
|
||||
helm.sh/chart: {{ include "redis.chart" . }}
|
||||
{{ include "redis.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "redis.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "redis.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
10
helm/core/charts/redis/templates/configmap.yaml
Normal file
10
helm/core/charts/redis/templates/configmap.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "redis.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
data:
|
||||
redis-stack.conf: |
|
||||
{{- if .Values.redis.password }}
|
||||
requirepass {{ .Values.redis.password }}
|
||||
{{- end }}
|
||||
16
helm/core/charts/redis/templates/pvc.yaml
Normal file
16
helm/core/charts/redis/templates/pvc.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "redis.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- range .Values.redis.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
storageClassName: {{ .Values.redis.persistence.storageClass }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.redis.persistence.size | quote }}
|
||||
{{- end }}
|
||||
15
helm/core/charts/redis/templates/service.yaml
Normal file
15
helm/core/charts/redis/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "redis.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "redis.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.redis.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.redis.service.port }}
|
||||
targetPort: 6379
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "redis.selectorLabels" . | nindent 4 }}
|
||||
74
helm/core/charts/redis/templates/statefulset.yaml
Normal file
74
helm/core/charts/redis/templates/statefulset.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "redis.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "redis.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.redis.replicas }}
|
||||
serviceName: {{ include "redis.name" . }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "redis.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "redis.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
{{- with .Values.global.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.global.hub }}/{{ .Values.redis.image | default "redis-stack-server" }}:{{ .Values.redis.tag | default .Chart.AppVersion }}"
|
||||
{{- if .Values.global.imagePullPolicy }}
|
||||
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 6379
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.redis.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /redis-stack.conf
|
||||
subPath: redis-stack.conf
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
- name: db
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
{{- with .Values.redis.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.redis.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.redis.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "redis.name" . }}
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
- name: db
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "redis.name" . }}
|
||||
{{- end }}
|
||||
48
helm/core/charts/redis/values.yaml
Normal file
48
helm/core/charts/redis/values.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
# Default values for redis.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
global:
|
||||
# -- Specify the image registry and pull policy
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
# -- Specify image pull policy if default behavior isn't desired.
|
||||
# Default behavior: latest images will be Always else IfNotPresent.
|
||||
imagePullPolicy: ""
|
||||
# -- Specify the image pull secrets
|
||||
imagePullSecrets: []
|
||||
|
||||
redis:
|
||||
# -- Specify the name
|
||||
name: redis-stack-server
|
||||
# -- Specify the image
|
||||
image: "redis-stack-server"
|
||||
# -- Specify the tag
|
||||
tag: "7.4.0-v3"
|
||||
# -- Specify the number of replicas
|
||||
replicas: 1
|
||||
# -- Specify the password, if not set, no password is used
|
||||
password: ""
|
||||
# -- Service parameters
|
||||
service:
|
||||
# -- Exporter service type
|
||||
type: ClusterIP
|
||||
# -- Exporter service port
|
||||
port: 6379
|
||||
# -- Specify the resources
|
||||
resources: {}
|
||||
# -- NodeSelector Node labels for Redis
|
||||
nodeSelector: {}
|
||||
# -- Tolerations for Redis
|
||||
tolerations: []
|
||||
# -- Affinity for Redis
|
||||
affinity: {}
|
||||
persistence:
|
||||
# -- Enable persistence on Redis
|
||||
enabled: false
|
||||
# -- If defined, storageClassName: <storageClass>
|
||||
# -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner
|
||||
storageClass: ""
|
||||
# -- Persistent Volume access modes
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
# -- Persistent Volume size
|
||||
size: 1Gi
|
||||
@@ -15,6 +15,9 @@ template:
|
||||
{{- with .Values.gateway.revision }}
|
||||
istio.io/rev: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.podLabels }}
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- include "gateway.selectorLabels" . | nindent 6 }}
|
||||
spec:
|
||||
{{- with .Values.gateway.imagePullSecrets }}
|
||||
@@ -42,9 +45,9 @@ template:
|
||||
- router
|
||||
- --domain
|
||||
- $(POD_NAMESPACE).svc.cluster.local
|
||||
- --proxyLogLevel=warning
|
||||
- --proxyComponentLogLevel=misc:error
|
||||
- --log_output_level=all:info
|
||||
- --proxyLogLevel={{- default "warning" .Values.global.proxy.logLevel }}
|
||||
- --proxyComponentLogLevel={{- default "misc:error" .Values.global.proxy.componentLogLevel }}
|
||||
- --log_output_level={{- default "default:info" .Values.global.logging.level }}
|
||||
- --serviceCluster=higress-gateway
|
||||
securityContext:
|
||||
{{- if .Values.gateway.containerSecurityContext }}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
{{- end }}
|
||||
proxyStatsMatcher:
|
||||
inclusionRegexps:
|
||||
- ".*"
|
||||
{{ toYaml .Values.global.proxy.proxyStatsMatcher.inclusionRegexps | indent 8 }}
|
||||
{{- end }}
|
||||
|
||||
{{/* We take the mesh config above, defined with individual values.yaml, and merge with .Values.meshConfig */}}
|
||||
|
||||
@@ -19,6 +19,9 @@ spec:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- with .Values.controller.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "controller.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.controller.imagePullSecrets }}
|
||||
@@ -136,6 +139,10 @@ spec:
|
||||
periodSeconds: 3
|
||||
timeoutSeconds: 5
|
||||
env:
|
||||
{{- if .Values.global.watchNamespace }}
|
||||
- name: ISTIO_WATCH_NAMESPACE
|
||||
value: "{{ .Values.global.watchNamespace }}"
|
||||
{{- end }}
|
||||
- name: ENABLE_PUSH_ALL_MCP_CLUSTERS
|
||||
value: "{{ .Values.global.enablePushAllMCPClusters }}"
|
||||
- name: PILOT_ENABLE_LDS_CACHE
|
||||
|
||||
@@ -9,6 +9,8 @@ global:
|
||||
xdsMaxRecvMsgSize: "104857600"
|
||||
defaultUpstreamConcurrencyThreshold: 10000
|
||||
enableSRDS: true
|
||||
# -- Whether to enable Redis(redis-stack-server) for Higress, default is false.
|
||||
enableRedis: false
|
||||
onDemandRDS: false
|
||||
hostRDSMergeSubset: false
|
||||
onlyPushRouteCluster: true
|
||||
@@ -199,6 +201,11 @@ global:
|
||||
# -- Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
|
||||
holdApplicationUntilProxyStarts: false
|
||||
|
||||
# -- Proxy stats name regexps matcher for inclusion
|
||||
proxyStatsMatcher:
|
||||
inclusionRegexps:
|
||||
- ".*"
|
||||
|
||||
proxy_init:
|
||||
# -- Base name for the proxy_init container, used to configure iptables.
|
||||
image: proxyv2
|
||||
@@ -462,6 +469,9 @@ gateway:
|
||||
prometheus.io/path: "/stats/prometheus"
|
||||
sidecar.istio.io/inject: "false"
|
||||
|
||||
# -- Labels to apply to the pod
|
||||
podLabels: {}
|
||||
|
||||
# -- Define the security context for the pod.
|
||||
# If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443.
|
||||
# On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl.
|
||||
@@ -488,6 +498,7 @@ gateway:
|
||||
externalTrafficPolicy: ""
|
||||
|
||||
rollingMaxSurge: 100%
|
||||
# -- If global.local is true, the default value is 100%, otherwise it is 25%
|
||||
rollingMaxUnavailable: 25%
|
||||
|
||||
resources:
|
||||
@@ -543,12 +554,12 @@ controller:
|
||||
labels: {}
|
||||
|
||||
probe:
|
||||
{
|
||||
httpGet: { path: /ready, port: 8888 },
|
||||
initialDelaySeconds: 1,
|
||||
periodSeconds: 3,
|
||||
timeoutSeconds: 5,
|
||||
}
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 8888
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 3
|
||||
timeoutSeconds: 5
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
@@ -566,21 +577,26 @@ controller:
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
# -- Labels to apply to the pod
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext:
|
||||
{}
|
||||
# fsGroup: 2000
|
||||
# fsGroup: 2000
|
||||
|
||||
ports:
|
||||
[
|
||||
{ "name": "http", "protocol": "TCP", "port": 8888, "targetPort": 8888 },
|
||||
{
|
||||
"name": "http-solver",
|
||||
"protocol": "TCP",
|
||||
"port": 8889,
|
||||
"targetPort": 8889,
|
||||
},
|
||||
{ "name": "grpc", "protocol": "TCP", "port": 15051, "targetPort": 15051 },
|
||||
]
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 8888
|
||||
targetPort: 8888
|
||||
- name: http-solver
|
||||
protocol: TCP
|
||||
port: 8889
|
||||
targetPort: 8889
|
||||
- name: grpc
|
||||
protocol: TCP
|
||||
port: 15051
|
||||
targetPort: 15051
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -590,9 +606,9 @@ controller:
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
resources:
|
||||
requests:
|
||||
@@ -715,3 +731,40 @@ downstream:
|
||||
upstream:
|
||||
idleTimeout: 10
|
||||
connectionBufferLimits: 10485760
|
||||
|
||||
redis:
|
||||
redis:
|
||||
name: redis-stack-server
|
||||
# -- Specify the image
|
||||
image: "redis-stack-server"
|
||||
# -- Specify the tag
|
||||
tag: "7.4.0-v3"
|
||||
# -- Specify the number of replicas
|
||||
replicas: 1
|
||||
# -- Specify the password, if not set, no password is used
|
||||
password: ""
|
||||
# -- Service parameters
|
||||
service:
|
||||
# -- Exporter service type
|
||||
type: ClusterIP
|
||||
# -- Exporter service port
|
||||
port: 6379
|
||||
# -- Specify the resources
|
||||
resources: {}
|
||||
# -- NodeSelector Node labels for Redis
|
||||
nodeSelector: {}
|
||||
# -- Tolerations for Redis
|
||||
tolerations: []
|
||||
# -- Affinity for Redis
|
||||
affinity: {}
|
||||
persistence:
|
||||
# -- Enable persistence on Redis, default is false
|
||||
enabled: false
|
||||
# -- If defined, storageClassName: <storageClass>
|
||||
# -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner
|
||||
storageClass: ""
|
||||
# -- Persistent Volume access modes
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
# -- Persistent Volume size
|
||||
size: 1Gi
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.0.7
|
||||
version: 2.1.1
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.0.4
|
||||
digest: sha256:ca9cc8bdac0488d79c20e7a4e3d7b3a436a59b697a37728daa462601b4d1ea65
|
||||
generated: "2025-02-19T16:23:39.424987+08:00"
|
||||
version: 2.1.1
|
||||
digest: sha256:a40f56252d0aa995fbf62952a6d23304dd84845caf5766bc64f7270cb00f01e9
|
||||
generated: "2025-04-18T16:45:23.961507+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.7
|
||||
appVersion: 2.1.1
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 2.0.7
|
||||
version: 2.1.1
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.0.4
|
||||
version: 2.1.1
|
||||
type: application
|
||||
version: 2.0.7
|
||||
version: 2.1.1
|
||||
|
||||
@@ -51,6 +51,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| controller.name | string | `"higress-controller"` | |
|
||||
| controller.nodeSelector | object | `{}` | |
|
||||
| controller.podAnnotations | object | `{}` | |
|
||||
| controller.podLabels | object | `{}` | Labels to apply to the pod |
|
||||
| controller.podSecurityContext | object | `{}` | |
|
||||
| controller.ports[0].name | string | `"http"` | |
|
||||
| controller.ports[0].port | int | `8888` | |
|
||||
@@ -115,6 +116,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.podAnnotations."prometheus.io/port" | string | `"15020"` | |
|
||||
| gateway.podAnnotations."prometheus.io/scrape" | string | `"true"` | |
|
||||
| gateway.podAnnotations."sidecar.istio.io/inject" | string | `"false"` | |
|
||||
| gateway.podLabels | object | `{}` | Labels to apply to the pod |
|
||||
| gateway.rbac.enabled | bool | `true` | If enabled, roles will be created to enable accessing certificates from Gateways. This is not needed when using http://gateway-api.org/. |
|
||||
| gateway.readinessFailureThreshold | int | `30` | The number of successive failed probes before indicating readiness failure. |
|
||||
| gateway.readinessInitialDelaySeconds | int | `1` | The initial delay for readiness probes in seconds. |
|
||||
@@ -128,7 +130,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | |
|
||||
| gateway.revision | string | `""` | revision declares which revision this gateway is a part of |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | If global.local is true, the default value is 100%, otherwise it is 25% |
|
||||
| gateway.securityContext | string | `nil` | Define the security context for the pod. If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443. On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl. |
|
||||
| gateway.service.annotations | object | `{}` | |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | |
|
||||
@@ -162,9 +164,10 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.enableH3 | bool | `false` | |
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableLDSCache | bool | `true` | |
|
||||
| global.enableLDSCache | 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. |
|
||||
| global.enableSRDS | bool | `true` | |
|
||||
| global.enableStatus | bool | `true` | If true, Higress Controller will update the status field of Ingress resources. When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the status field of the corresponding Ingress object. |
|
||||
| global.externalIstiod | bool | `false` | Configure a remote cluster data plane controlled by an external istiod. When set to true, istiod is not deployed locally and only a subset of the other discovery charts are enabled. |
|
||||
@@ -209,6 +212,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.proxy.includeOutboundPorts | string | `""` | |
|
||||
| global.proxy.logLevel | string | `"warning"` | Log level for proxy, applies to gateways and sidecars. Expected values are: trace|debug|info|warning|error|critical|off |
|
||||
| global.proxy.privileged | bool | `false` | If set to true, istio-proxy container will have privileged securityContext |
|
||||
| global.proxy.proxyStatsMatcher | object | `{"inclusionRegexps":[".*"]}` | Proxy stats name regexps matcher for inclusion |
|
||||
| global.proxy.readinessFailureThreshold | int | `30` | The number of successive failed probes before indicating readiness failure. |
|
||||
| global.proxy.readinessInitialDelaySeconds | int | `1` | The initial delay for readiness probes in seconds. |
|
||||
| global.proxy.readinessPeriodSeconds | int | `2` | The period between readiness probes. |
|
||||
@@ -269,6 +273,22 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| pilot.serviceAnnotations | object | `{}` | |
|
||||
| pilot.tag | string | `""` | |
|
||||
| pilot.traceSampling | float | `1` | |
|
||||
| redis.redis.affinity | object | `{}` | Affinity for Redis |
|
||||
| redis.redis.image | string | `"redis-stack-server"` | Specify the image |
|
||||
| redis.redis.name | string | `"redis-stack-server"` | |
|
||||
| redis.redis.nodeSelector | object | `{}` | NodeSelector Node labels for Redis |
|
||||
| redis.redis.password | string | `""` | Specify the password, if not set, no password is used |
|
||||
| redis.redis.persistence.accessModes | list | `["ReadWriteOnce"]` | Persistent Volume access modes |
|
||||
| redis.redis.persistence.enabled | bool | `false` | Enable persistence on Redis, default is false |
|
||||
| redis.redis.persistence.size | string | `"1Gi"` | Persistent Volume size |
|
||||
| redis.redis.persistence.storageClass | string | `""` | If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner |
|
||||
| redis.redis.replicas | int | `1` | Specify the number of replicas |
|
||||
| redis.redis.resources | object | `{}` | Specify the resources |
|
||||
| redis.redis.service | object | `{"port":6379,"type":"ClusterIP"}` | Service parameters |
|
||||
| redis.redis.service.port | int | `6379` | Exporter service port |
|
||||
| redis.redis.service.type | string | `"ClusterIP"` | Exporter service type |
|
||||
| redis.redis.tag | string | `"7.4.0-v3"` | Specify the tag |
|
||||
| redis.redis.tolerations | list | `[]` | Tolerations for Redis |
|
||||
| revision | string | `""` | |
|
||||
| tracing.enable | bool | `false` | |
|
||||
| tracing.sampling | int | `100` | |
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
module github.com/alibaba/higress/hgctl
|
||||
|
||||
go 1.21.0
|
||||
|
||||
toolchain go1.22.2
|
||||
go 1.22.2
|
||||
|
||||
toolchain go1.23.7
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
|
||||
Submodule istio/istio updated: 5a5aa495c7...e6578f7dd0
Submodule istio/proxy updated: 8a85c12b89...862975858e
@@ -235,7 +235,7 @@ func (s *Server) initConfigController() error {
|
||||
options.ClusterId = ""
|
||||
}
|
||||
|
||||
ingressConfig := translation.NewIngressTranslation(s.kubeClient, s.xdsServer, ns, options.ClusterId)
|
||||
ingressConfig := translation.NewIngressTranslation(s.kubeClient, s.xdsServer, ns, options)
|
||||
ingressConfig.AddLocalCluster(options)
|
||||
|
||||
s.configStores = append(s.configStores, ingressConfig)
|
||||
|
||||
@@ -151,9 +151,37 @@ type IngressConfig struct {
|
||||
clusterId cluster.ID
|
||||
|
||||
httpsConfigMgr *cert.ConfigMgr
|
||||
|
||||
// templateProcessor processes template variables in config
|
||||
templateProcessor *TemplateProcessor
|
||||
|
||||
// secretConfigMgr manages secret dependencies
|
||||
secretConfigMgr *SecretConfigMgr
|
||||
}
|
||||
|
||||
func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, clusterId cluster.ID) *IngressConfig {
|
||||
// getSecretValue implements the getValue function for secret references
|
||||
func (m *IngressConfig) getSecretValue(valueType, namespace, name, key string) (string, error) {
|
||||
if valueType != "secret" {
|
||||
return "", fmt.Errorf("unsupported value type: %s", valueType)
|
||||
}
|
||||
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, controller := range m.remoteIngressControllers {
|
||||
secret, err := controller.SecretLister().Secrets(namespace).Get(name)
|
||||
if err == nil {
|
||||
if value, exists := secret.Data[key]; exists {
|
||||
return string(value), nil
|
||||
}
|
||||
return "", fmt.Errorf("key %s not found in secret %s/%s", key, namespace, name)
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("secret %s/%s not found", namespace, name)
|
||||
}
|
||||
|
||||
func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *IngressConfig {
|
||||
clusterId := options.ClusterId
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
@@ -170,17 +198,24 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
|
||||
wasmPlugins: make(map[string]*extensions.WasmPlugin),
|
||||
http2rpcs: make(map[string]*higressv1.Http2Rpc),
|
||||
}
|
||||
mcpbridgeController := mcpbridge.NewController(localKubeClient, clusterId)
|
||||
|
||||
// Initialize secret config manager
|
||||
config.secretConfigMgr = NewSecretConfigMgr(xdsUpdater)
|
||||
|
||||
// Initialize template processor with value getter function
|
||||
config.templateProcessor = NewTemplateProcessor(config.getSecretValue, namespace, config.secretConfigMgr)
|
||||
|
||||
mcpbridgeController := mcpbridge.NewController(localKubeClient, options)
|
||||
mcpbridgeController.AddEventHandler(config.AddOrUpdateMcpBridge, config.DeleteMcpBridge)
|
||||
config.mcpbridgeController = mcpbridgeController
|
||||
config.mcpbridgeLister = mcpbridgeController.Lister()
|
||||
|
||||
wasmPluginController := wasmplugin.NewController(localKubeClient, clusterId)
|
||||
wasmPluginController := wasmplugin.NewController(localKubeClient, options)
|
||||
wasmPluginController.AddEventHandler(config.AddOrUpdateWasmPlugin, config.DeleteWasmPlugin)
|
||||
config.wasmPluginController = wasmPluginController
|
||||
config.wasmPluginLister = wasmPluginController.Lister()
|
||||
|
||||
http2rpcController := http2rpc.NewController(localKubeClient, clusterId)
|
||||
http2rpcController := http2rpc.NewController(localKubeClient, options)
|
||||
http2rpcController.AddEventHandler(config.AddOrUpdateHttp2Rpc, config.DeleteHttp2Rpc)
|
||||
config.http2rpcController = http2rpcController
|
||||
config.http2rpcLister = http2rpcController.Lister()
|
||||
@@ -225,8 +260,9 @@ func (m *IngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f ist
|
||||
}
|
||||
|
||||
func (m *IngressConfig) AddLocalCluster(options common.Options) {
|
||||
secretController := secret.NewController(m.localKubeClient, options.ClusterId)
|
||||
secretController := secret.NewController(m.localKubeClient, options)
|
||||
secretController.AddEventHandler(m.ReflectSecretChanges)
|
||||
secretController.AddEventHandler(m.secretConfigMgr.HandleSecretChange)
|
||||
|
||||
var ingressController common.IngressController
|
||||
v1 := common.V1Available(m.localKubeClient)
|
||||
@@ -253,10 +289,24 @@ func (m *IngressConfig) List(typ config.GroupVersionKind, namespace string) []co
|
||||
var configs = make([]config.Config, 0)
|
||||
|
||||
if configsFromIngress := m.listFromIngressControllers(typ, namespace); configsFromIngress != nil {
|
||||
// Process templates for ingress configs
|
||||
for i := range configsFromIngress {
|
||||
if err := m.templateProcessor.ProcessConfig(&configsFromIngress[i]); err != nil {
|
||||
IngressLog.Errorf("Failed to process template for config %s/%s: %v",
|
||||
configsFromIngress[i].Namespace, configsFromIngress[i].Name, err)
|
||||
}
|
||||
}
|
||||
configs = append(configs, configsFromIngress...)
|
||||
}
|
||||
|
||||
if configsFromGateway := m.listFromGatewayControllers(typ, namespace); configsFromGateway != nil {
|
||||
// Process templates for gateway configs
|
||||
for i := range configsFromGateway {
|
||||
if err := m.templateProcessor.ProcessConfig(&configsFromGateway[i]); err != nil {
|
||||
IngressLog.Errorf("Failed to process template for config %s/%s: %v",
|
||||
configsFromGateway[i].Namespace, configsFromGateway[i].Name, err)
|
||||
}
|
||||
}
|
||||
configs = append(configs, configsFromGateway...)
|
||||
}
|
||||
|
||||
@@ -986,7 +1036,6 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
return nil, nil
|
||||
}
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
func isBoolValueTrue(b *wrappers.BoolValue) bool {
|
||||
|
||||
@@ -127,7 +127,14 @@ func TestConvertGatewaysForIngress(t *testing.T) {
|
||||
}
|
||||
ingressV1Beta1Controller := controllerv1beta1.NewController(fake, fake, v1Beta1Options, nil)
|
||||
ingressV1Controller := controllerv1.NewController(fake, fake, v1Options, nil)
|
||||
m := NewIngressConfig(fake, nil, "wakanda", "gw-123-istio")
|
||||
options := common.Options{
|
||||
Enable: true,
|
||||
ClusterId: "gw-123-istio",
|
||||
RawClusterId: "gw-123-istio__",
|
||||
GatewayHttpPort: 80,
|
||||
GatewayHttpsPort: 443,
|
||||
}
|
||||
m := NewIngressConfig(fake, nil, "wakanda", options)
|
||||
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
|
||||
"ingress-v1beta1": ingressV1Beta1Controller,
|
||||
"ingress-v1": ingressV1Controller,
|
||||
|
||||
119
pkg/ingress/config/ingress_template.go
Normal file
119
pkg/ingress/config/ingress_template.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"istio.io/istio/pkg/config"
|
||||
)
|
||||
|
||||
// TemplateProcessor handles template substitution in configs
|
||||
type TemplateProcessor struct {
|
||||
// getValue is a function that retrieves values by type, namespace, name and key
|
||||
getValue func(valueType, namespace, name, key string) (string, error)
|
||||
namespace string
|
||||
secretConfigMgr *SecretConfigMgr
|
||||
}
|
||||
|
||||
// NewTemplateProcessor creates a new TemplateProcessor with the given value getter function
|
||||
func NewTemplateProcessor(getValue func(valueType, namespace, name, key string) (string, error), namespace string, secretConfigMgr *SecretConfigMgr) *TemplateProcessor {
|
||||
return &TemplateProcessor{
|
||||
getValue: getValue,
|
||||
namespace: namespace,
|
||||
secretConfigMgr: secretConfigMgr,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessConfig processes a config and substitutes any template variables
|
||||
func (p *TemplateProcessor) ProcessConfig(cfg *config.Config) error {
|
||||
// Convert spec to JSON string to process substitutions
|
||||
jsonBytes, err := json.Marshal(cfg.Spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config spec: %v", err)
|
||||
}
|
||||
|
||||
configStr := string(jsonBytes)
|
||||
// Find all value references in format:
|
||||
// ${type.name.key} or ${type.namespace/name.key}
|
||||
valueRegex := regexp.MustCompile(`\$\{([^.}]+)\.(?:([^/]+)/)?([^.}]+)\.([^}]+)\}`)
|
||||
matches := valueRegex.FindAllStringSubmatch(configStr, -1)
|
||||
// If there are no value references, return immediately
|
||||
if len(matches) == 0 {
|
||||
if p.secretConfigMgr != nil {
|
||||
if err := p.secretConfigMgr.DeleteConfig(cfg); err != nil {
|
||||
IngressLog.Errorf("failed to delete secret dependency: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
foundSecretSource := false
|
||||
IngressLog.Infof("start to apply config %s/%s with %d variables", cfg.Namespace, cfg.Name, len(matches))
|
||||
for _, match := range matches {
|
||||
valueType := match[1]
|
||||
var namespace, name, key string
|
||||
if match[2] != "" {
|
||||
// Format: ${type.namespace/name.key}
|
||||
namespace = match[2]
|
||||
} else {
|
||||
// Format: ${type.name.key} - use default namespace
|
||||
namespace = p.namespace
|
||||
}
|
||||
name = match[3]
|
||||
key = match[4]
|
||||
|
||||
// Get value using the provided getter function
|
||||
value, err := p.getValue(valueType, namespace, name, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %s value for %s/%s.%s: %v", valueType, namespace, name, key, err)
|
||||
}
|
||||
|
||||
// Add secret dependency if this is a secret reference
|
||||
if valueType == "secret" && p.secretConfigMgr != nil {
|
||||
foundSecretSource = true
|
||||
secretKey := fmt.Sprintf("%s/%s", namespace, name)
|
||||
if err := p.secretConfigMgr.AddConfig(secretKey, cfg); err != nil {
|
||||
IngressLog.Errorf("failed to add secret dependency: %v", err)
|
||||
}
|
||||
}
|
||||
// Replace placeholder with actual value
|
||||
configStr = strings.Replace(configStr, match[0], value, 1)
|
||||
}
|
||||
|
||||
// Create a new instance of the same type as cfg.Spec
|
||||
newSpec := proto.Clone(cfg.Spec.(proto.Message))
|
||||
if err := json.Unmarshal([]byte(configStr), newSpec); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal substituted config: %v", err)
|
||||
}
|
||||
cfg.Spec = newSpec
|
||||
|
||||
// Delete secret dependency if no secret reference is found
|
||||
if !foundSecretSource {
|
||||
if p.secretConfigMgr != nil {
|
||||
if err := p.secretConfigMgr.DeleteConfig(cfg); err != nil {
|
||||
IngressLog.Errorf("failed to delete secret dependency: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IngressLog.Infof("end to process config %s/%s", cfg.Namespace, cfg.Name)
|
||||
return nil
|
||||
}
|
||||
166
pkg/ingress/config/ingress_template_test.go
Normal file
166
pkg/ingress/config/ingress_template_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
extensions "istio.io/api/extensions/v1alpha1"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
)
|
||||
|
||||
func TestTemplateProcessor_ProcessConfig(t *testing.T) {
|
||||
// Create test values map
|
||||
values := map[string]string{
|
||||
"secret.default/test-secret.api_key": "test-api-key",
|
||||
"secret.default/test-secret.plugin_conf.timeout": "5000",
|
||||
"secret.default/test-secret.plugin_conf.max_retries": "3",
|
||||
"secret.higress-system/auth-secret.auth_config.type": "basic",
|
||||
"secret.higress-system/auth-secret.auth_config.credentials": "base64-encoded",
|
||||
}
|
||||
|
||||
// Mock value getter function
|
||||
getValue := func(valueType, namespace, name, key string) (string, error) {
|
||||
fullKey := fmt.Sprintf("%s.%s/%s.%s", valueType, namespace, name, key)
|
||||
fmt.Printf("Getting value for %s", fullKey)
|
||||
if value, exists := values[fullKey]; exists {
|
||||
return value, nil
|
||||
}
|
||||
return "", fmt.Errorf("value not found for %s", fullKey)
|
||||
}
|
||||
|
||||
// Create template processor
|
||||
processor := NewTemplateProcessor(getValue, "higress-system", nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wasmPlugin *extensions.WasmPlugin
|
||||
expected *extensions.WasmPlugin
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple api key reference",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"api_key": "${secret.default/test-secret.api_key}",
|
||||
}),
|
||||
},
|
||||
expected: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"api_key": "test-api-key",
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "config with multiple fields",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"timeout": "${secret.default/test-secret.plugin_conf.timeout}",
|
||||
"max_retries": "${secret.default/test-secret.plugin_conf.max_retries}",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expected: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"timeout": "5000",
|
||||
"max_retries": "3",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "auth config with default namespace",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"auth": map[string]interface{}{
|
||||
"type": "${secret.auth-secret.auth_config.type}",
|
||||
"credentials": "${secret.auth-secret.auth_config.credentials}",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expected: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"auth": map[string]interface{}{
|
||||
"type": "basic",
|
||||
"credentials": "base64-encoded",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent secret",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"api_key": "${secret.default/non-existent.api_key}",
|
||||
}),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: tt.wasmPlugin,
|
||||
}
|
||||
|
||||
err := processor.ProcessConfig(cfg)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
processedPlugin := cfg.Spec.(*extensions.WasmPlugin)
|
||||
|
||||
// Compare plugin name
|
||||
assert.Equal(t, tt.expected.PluginName, processedPlugin.PluginName)
|
||||
|
||||
// Compare plugin configs
|
||||
if tt.expected.PluginConfig != nil {
|
||||
assert.NotNil(t, processedPlugin.PluginConfig)
|
||||
assert.Equal(t, tt.expected.PluginConfig.AsMap(), processedPlugin.PluginConfig.AsMap())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create structpb.Struct from map
|
||||
func makeStructValue(t *testing.T, m map[string]interface{}) *structpb.Struct {
|
||||
s, err := structpb.NewStruct(m)
|
||||
assert.NoError(t, err, "Failed to create struct value")
|
||||
return s
|
||||
}
|
||||
@@ -75,10 +75,11 @@ type KIngressConfig struct {
|
||||
clusterId cluster.ID
|
||||
}
|
||||
|
||||
func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater istiomodel.XDSUpdater, namespace string, clusterId cluster.ID) *KIngressConfig {
|
||||
func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *KIngressConfig {
|
||||
if localKubeClient.KIngressInformer() == nil {
|
||||
return nil
|
||||
}
|
||||
clusterId := options.ClusterId
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
@@ -114,7 +115,7 @@ func (m *KIngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f is
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) AddLocalCluster(options common.Options) common.KIngressController {
|
||||
secretController := secret.NewController(m.localKubeClient, options.ClusterId)
|
||||
secretController := secret.NewController(m.localKubeClient, options)
|
||||
secretController.AddEventHandler(m.ReflectSecretChanges)
|
||||
|
||||
var ingressController common.KIngressController
|
||||
|
||||
@@ -118,7 +118,14 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
|
||||
RawClusterId: "kingress__",
|
||||
}
|
||||
kingressV1Controller := kcontrollerv1.NewController(fake, fake, v1Options, nil)
|
||||
m := NewKIngressConfig(fake, nil, "wakanda", "gw-123-istio")
|
||||
options := common.Options{
|
||||
Enable: true,
|
||||
ClusterId: "gw-123-istio",
|
||||
RawClusterId: "gw-123-istio__",
|
||||
GatewayHttpPort: 80,
|
||||
GatewayHttpsPort: 443,
|
||||
}
|
||||
m := NewKIngressConfig(fake, nil, "wakanda", options)
|
||||
m.remoteIngressControllers = map[cluster.ID]common.KIngressController{
|
||||
"kingress": kingressV1Controller,
|
||||
}
|
||||
|
||||
157
pkg/ingress/config/secret_config_mgr.go
Normal file
157
pkg/ingress/config/secret_config_mgr.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
istiomodel "istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
)
|
||||
|
||||
// toConfigKey converts config.Config to istiomodel.ConfigKey
|
||||
func toConfigKey(cfg *config.Config) (istiomodel.ConfigKey, error) {
|
||||
return istiomodel.ConfigKey{
|
||||
Kind: kind.MustFromGVK(cfg.GroupVersionKind),
|
||||
Name: cfg.Name,
|
||||
Namespace: cfg.Namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SecretConfigMgr maintains the mapping between secrets and configs
|
||||
type SecretConfigMgr struct {
|
||||
mutex sync.RWMutex
|
||||
|
||||
// configSet tracks all configs that have been added
|
||||
// key format: namespace/name
|
||||
configSet sets.Set[string]
|
||||
|
||||
// secretToConfigs maps secret key to dependent configs
|
||||
// key format: namespace/name
|
||||
secretToConfigs map[string]sets.Set[istiomodel.ConfigKey]
|
||||
|
||||
// watchedSecrets tracks which secrets are being watched
|
||||
watchedSecrets sets.Set[string]
|
||||
|
||||
// xdsUpdater is used to push config updates
|
||||
xdsUpdater istiomodel.XDSUpdater
|
||||
}
|
||||
|
||||
// NewSecretConfigMgr creates a new SecretConfigMgr
|
||||
func NewSecretConfigMgr(xdsUpdater istiomodel.XDSUpdater) *SecretConfigMgr {
|
||||
return &SecretConfigMgr{
|
||||
secretToConfigs: make(map[string]sets.Set[istiomodel.ConfigKey]),
|
||||
watchedSecrets: sets.New[string](),
|
||||
configSet: sets.New[string](),
|
||||
xdsUpdater: xdsUpdater,
|
||||
}
|
||||
}
|
||||
|
||||
// AddConfig adds a config and its secret dependencies
|
||||
func (m *SecretConfigMgr) AddConfig(secretKey string, cfg *config.Config) error {
|
||||
configKey, _ := toConfigKey(cfg)
|
||||
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
configId := fmt.Sprintf("%s/%s", cfg.Namespace, cfg.Name)
|
||||
m.configSet.Insert(configId)
|
||||
|
||||
if configs, exists := m.secretToConfigs[secretKey]; exists {
|
||||
configs.Insert(configKey)
|
||||
} else {
|
||||
m.secretToConfigs[secretKey] = sets.New(configKey)
|
||||
}
|
||||
|
||||
// Add to watched secrets
|
||||
m.watchedSecrets.Insert(secretKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteConfig removes a config from all secret dependencies
|
||||
func (m *SecretConfigMgr) DeleteConfig(cfg *config.Config) error {
|
||||
configKey, _ := toConfigKey(cfg)
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
configId := fmt.Sprintf("%s/%s", cfg.Namespace, cfg.Name)
|
||||
if !m.configSet.Contains(configId) {
|
||||
return nil
|
||||
}
|
||||
|
||||
removeKeys := make([]string, 0)
|
||||
// Find and remove the config from all secrets
|
||||
for secretKey, configs := range m.secretToConfigs {
|
||||
if configs.Contains(configKey) {
|
||||
configs.Delete(configKey)
|
||||
// If no more configs depend on this secret, remove it
|
||||
if configs.Len() == 0 {
|
||||
removeKeys = append(removeKeys, secretKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the secrets from the secretToConfigs map
|
||||
for _, secretKey := range removeKeys {
|
||||
delete(m.secretToConfigs, secretKey)
|
||||
m.watchedSecrets.Delete(secretKey)
|
||||
}
|
||||
// Remove the config from the config set
|
||||
m.configSet.Delete(configId)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigsForSecret returns all configs that depend on the given secret
|
||||
func (m *SecretConfigMgr) GetConfigsForSecret(secretKey string) []istiomodel.ConfigKey {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if configs, exists := m.secretToConfigs[secretKey]; exists {
|
||||
return configs.UnsortedList()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSecretWatched checks if a secret is being watched
|
||||
func (m *SecretConfigMgr) IsSecretWatched(secretKey string) bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.watchedSecrets.Contains(secretKey)
|
||||
}
|
||||
|
||||
// HandleSecretChange handles secret changes and updates affected configs
|
||||
func (m *SecretConfigMgr) HandleSecretChange(name util.ClusterNamespacedName) {
|
||||
secretKey := fmt.Sprintf("%s/%s", name.Namespace, name.Name)
|
||||
// Check if this secret is being watched
|
||||
if !m.IsSecretWatched(secretKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get affected configs
|
||||
configKeys := m.GetConfigsForSecret(secretKey)
|
||||
if len(configKeys) == 0 {
|
||||
return
|
||||
}
|
||||
IngressLog.Infof("SecretConfigMgr Secret %s changed, updating %d dependent configs and push", secretKey, len(configKeys))
|
||||
m.xdsUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
Reason: istiomodel.NewReasonStats(istiomodel.SecretTrigger),
|
||||
})
|
||||
}
|
||||
155
pkg/ingress/config/secret_config_mgr_test.go
Normal file
155
pkg/ingress/config/secret_config_mgr_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
istiomodel "istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
)
|
||||
|
||||
type mockXdsUpdater struct {
|
||||
lastPushRequest *istiomodel.PushRequest
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) EDSUpdate(shard istiomodel.ShardKey, hostname string, namespace string, entry []*istiomodel.IstioEndpoint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) EDSCacheUpdate(shard istiomodel.ShardKey, hostname string, namespace string, entry []*istiomodel.IstioEndpoint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) SvcUpdate(shard istiomodel.ShardKey, hostname string, namespace string, event istiomodel.Event) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) ProxyUpdate(clusterID cluster.ID, ip string) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) RemoveShard(shardKey istiomodel.ShardKey) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) ConfigUpdate(req *istiomodel.PushRequest) {
|
||||
m.lastPushRequest = req
|
||||
}
|
||||
|
||||
func TestSecretConfigMgr(t *testing.T) {
|
||||
updater := &mockXdsUpdater{}
|
||||
mgr := NewSecretConfigMgr(updater)
|
||||
|
||||
// Test AddConfig
|
||||
t.Run("AddConfig", func(t *testing.T) {
|
||||
wasmPlugin := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
err := mgr.AddConfig("default/test-secret", wasmPlugin)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, mgr.IsSecretWatched("default/test-secret"))
|
||||
|
||||
configs := mgr.GetConfigsForSecret("default/test-secret")
|
||||
assert.Len(t, configs, 1)
|
||||
assert.Equal(t, kind.WasmPlugin, configs[0].Kind)
|
||||
assert.Equal(t, "test-plugin", configs[0].Name)
|
||||
assert.Equal(t, "default", configs[0].Namespace)
|
||||
})
|
||||
|
||||
// Test DeleteConfig
|
||||
t.Run("DeleteConfig", func(t *testing.T) {
|
||||
wasmPlugin := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
err := mgr.DeleteConfig(wasmPlugin)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, mgr.IsSecretWatched("default/test-secret"))
|
||||
assert.Empty(t, mgr.GetConfigsForSecret("default/test-secret"))
|
||||
})
|
||||
|
||||
// Test HandleSecretChange
|
||||
t.Run("HandleSecretChange", func(t *testing.T) {
|
||||
// Add a config first
|
||||
wasmPlugin := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := mgr.AddConfig("default/test-secret", wasmPlugin)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test secret change
|
||||
secretName := util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
mgr.HandleSecretChange(secretName)
|
||||
assert.NotNil(t, updater.lastPushRequest)
|
||||
assert.True(t, updater.lastPushRequest.Full)
|
||||
})
|
||||
|
||||
// Test full push for secret update
|
||||
t.Run("FullPushForSecretUpdate", func(t *testing.T) {
|
||||
// Add a secret config
|
||||
secretConfig := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Secret,
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := mgr.AddConfig("default/test-secret", secretConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update the secret
|
||||
secretName := util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
mgr.HandleSecretChange(secretName)
|
||||
assert.NotNil(t, updater.lastPushRequest)
|
||||
assert.True(t, updater.lastPushRequest.Full)
|
||||
})
|
||||
}
|
||||
@@ -40,6 +40,7 @@ type HigressConfig struct {
|
||||
Upstream *Upstream `json:"upstream,omitempty"`
|
||||
DisableXEnvoyHeaders bool `json:"disableXEnvoyHeaders,omitempty"`
|
||||
AddXRealIpHeader bool `json:"addXRealIpHeader,omitempty"`
|
||||
McpServer *McpServer `json:"mcpServer,omitempty"`
|
||||
}
|
||||
|
||||
func NewDefaultHigressConfig() *HigressConfig {
|
||||
@@ -51,6 +52,7 @@ func NewDefaultHigressConfig() *HigressConfig {
|
||||
Upstream: globalOption.Upstream,
|
||||
DisableXEnvoyHeaders: globalOption.DisableXEnvoyHeaders,
|
||||
AddXRealIpHeader: globalOption.AddXRealIpHeader,
|
||||
McpServer: NewDefaultMcpServer(),
|
||||
}
|
||||
return higressConfig
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ func NewConfigmapMgr(XDSUpdater model.XDSUpdater, namespace string, higressConfi
|
||||
globalOptionController := NewGlobalOptionController(namespace)
|
||||
configmapMgr.AddItemControllers(globalOptionController)
|
||||
|
||||
mcpServerController := NewMcpServerController(namespace)
|
||||
configmapMgr.AddItemControllers(mcpServerController)
|
||||
|
||||
configmapMgr.initEventHandlers()
|
||||
|
||||
return configmapMgr
|
||||
|
||||
433
pkg/ingress/kube/configmap/mcp_server.go
Normal file
433
pkg/ingress/kube/configmap/mcp_server.go
Normal file
@@ -0,0 +1,433 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
)
|
||||
|
||||
// RedisConfig defines the configuration for Redis connection
|
||||
type RedisConfig struct {
|
||||
// The address of Redis server in the format of "host:port"
|
||||
Address string `json:"address,omitempty"`
|
||||
// The username for Redis authentication
|
||||
Username string `json:"username,omitempty"`
|
||||
// The password for Redis authentication
|
||||
Password string `json:"password,omitempty"`
|
||||
// The database index to use
|
||||
DB int `json:"db,omitempty"`
|
||||
}
|
||||
|
||||
// MCPRatelimitConfig defines the configuration for rate limit
|
||||
type MCPRatelimitConfig struct {
|
||||
// The limit of the rate limit
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
// The window of the rate limit
|
||||
Window int64 `json:"window,omitempty"`
|
||||
// The white list of the rate limit
|
||||
WhiteList []string `json:"white_list,omitempty"`
|
||||
}
|
||||
|
||||
// SSEServer defines the configuration for Server-Sent Events (SSE) server
|
||||
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)
|
||||
Path string `json:"path,omitempty"`
|
||||
// The type of the SSE server
|
||||
Type string `json:"type,omitempty"`
|
||||
// Additional Config parameters for the real MCP server implementation
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
// MatchRule defines a rule for matching requests
|
||||
type MatchRule struct {
|
||||
// Domain pattern, supports wildcards
|
||||
MatchRuleDomain string `json:"match_rule_domain,omitempty"`
|
||||
// Path pattern to match
|
||||
MatchRulePath string `json:"match_rule_path,omitempty"`
|
||||
// Type of match rule: exact, prefix, suffix, contains, regex
|
||||
MatchRuleType string `json:"match_rule_type,omitempty"`
|
||||
}
|
||||
|
||||
// McpServer defines the configuration for MCP (Model Context Protocol) server
|
||||
type McpServer struct {
|
||||
// Flag to control whether MCP server is enabled
|
||||
Enable bool `json:"enable,omitempty"`
|
||||
// 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"`
|
||||
// List of SSE servers Configs
|
||||
Servers []*SSEServer `json:"servers,omitempty"`
|
||||
// List of match rules for filtering requests
|
||||
MatchList []*MatchRule `json:"match_list,omitempty"`
|
||||
// Flag to control whether user level server is enabled
|
||||
EnableUserLevelServer bool `json:"enable_user_level_server,omitempty"`
|
||||
// Rate limit config for MCP server
|
||||
Ratelimit *MCPRatelimitConfig `json:"rate_limit,omitempty"`
|
||||
}
|
||||
|
||||
func NewDefaultMcpServer() *McpServer {
|
||||
return &McpServer{
|
||||
Enable: false,
|
||||
Servers: make([]*SSEServer, 0),
|
||||
MatchList: make([]*MatchRule, 0),
|
||||
EnableUserLevelServer: false,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
higressMcpServerEnvoyFilterName = "higress-config-mcp-server"
|
||||
)
|
||||
|
||||
func validMcpServer(m *McpServer) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.EnableUserLevelServer && m.Redis == nil {
|
||||
return errors.New("redis config cannot be empty when user level server is enabled")
|
||||
}
|
||||
|
||||
// Validate match rule types
|
||||
if m.MatchList != nil {
|
||||
validTypes := map[string]bool{
|
||||
"exact": true,
|
||||
"prefix": true,
|
||||
"suffix": true,
|
||||
"contains": true,
|
||||
"regex": 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] {
|
||||
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareMcpServer(old *McpServer, new *McpServer) (Result, error) {
|
||||
if old == nil && new == nil {
|
||||
return ResultNothing, nil
|
||||
}
|
||||
|
||||
if new == nil {
|
||||
return ResultDelete, nil
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(old, new) {
|
||||
return ResultReplace, nil
|
||||
}
|
||||
|
||||
return ResultNothing, nil
|
||||
}
|
||||
|
||||
func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
newMcp := NewDefaultMcpServer()
|
||||
newMcp.Enable = mcp.Enable
|
||||
|
||||
if mcp.Redis != nil {
|
||||
newMcp.Redis = &RedisConfig{
|
||||
Address: mcp.Redis.Address,
|
||||
Username: mcp.Redis.Username,
|
||||
Password: mcp.Redis.Password,
|
||||
DB: mcp.Redis.DB,
|
||||
}
|
||||
}
|
||||
if mcp.Ratelimit != nil {
|
||||
newMcp.Ratelimit = &MCPRatelimitConfig{
|
||||
Limit: mcp.Ratelimit.Limit,
|
||||
Window: mcp.Ratelimit.Window,
|
||||
WhiteList: mcp.Ratelimit.WhiteList,
|
||||
}
|
||||
}
|
||||
newMcp.SsePathSuffix = mcp.SsePathSuffix
|
||||
|
||||
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
|
||||
|
||||
if len(mcp.Servers) > 0 {
|
||||
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
newServer := &SSEServer{
|
||||
Name: server.Name,
|
||||
Path: server.Path,
|
||||
Type: server.Type,
|
||||
}
|
||||
if server.Config != nil {
|
||||
newServer.Config = make(map[string]interface{})
|
||||
for k, v := range server.Config {
|
||||
newServer.Config[k] = v
|
||||
}
|
||||
}
|
||||
newMcp.Servers[i] = newServer
|
||||
}
|
||||
}
|
||||
|
||||
if len(mcp.MatchList) > 0 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newMcp, nil
|
||||
}
|
||||
|
||||
type McpServerController struct {
|
||||
Namespace string
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
}
|
||||
|
||||
func NewMcpServerController(namespace string) *McpServerController {
|
||||
mcpController := &McpServerController{
|
||||
Namespace: namespace,
|
||||
mcpServer: atomic.Value{},
|
||||
Name: "mcpServer",
|
||||
}
|
||||
mcpController.SetMcpServer(NewDefaultMcpServer())
|
||||
return mcpController
|
||||
}
|
||||
|
||||
func (m *McpServerController) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
func (m *McpServerController) SetMcpServer(mcp *McpServer) {
|
||||
m.mcpServer.Store(mcp)
|
||||
}
|
||||
|
||||
func (m *McpServerController) GetMcpServer() *McpServer {
|
||||
value := m.mcpServer.Load()
|
||||
if value != nil {
|
||||
if mcp, ok := value.(*McpServer); ok {
|
||||
return mcp
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *McpServerController) AddOrUpdateHigressConfig(name util.ClusterNamespacedName, old *HigressConfig, new *HigressConfig) error {
|
||||
if err := validMcpServer(new.McpServer); err != nil {
|
||||
IngressLog.Errorf("data:%+v convert to mcp server, error: %+v", new.McpServer, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
result, _ := compareMcpServer(old.McpServer, new.McpServer)
|
||||
|
||||
switch result {
|
||||
case ResultReplace:
|
||||
if newMcp, err := deepCopyMcpServer(new.McpServer); err != nil {
|
||||
IngressLog.Infof("mcp server deepcopy error:%v", err)
|
||||
} else {
|
||||
m.SetMcpServer(newMcp)
|
||||
IngressLog.Infof("AddOrUpdate Higress config mcp server")
|
||||
m.eventHandler(higressMcpServerEnvoyFilterName)
|
||||
IngressLog.Infof("send event with filter name:%s", higressMcpServerEnvoyFilterName)
|
||||
}
|
||||
case ResultDelete:
|
||||
m.SetMcpServer(NewDefaultMcpServer())
|
||||
IngressLog.Infof("Delete Higress config mcp server")
|
||||
m.eventHandler(higressMcpServerEnvoyFilterName)
|
||||
IngressLog.Infof("send event with filter name:%s", higressMcpServerEnvoyFilterName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *McpServerController) ValidHigressConfig(higressConfig *HigressConfig) error {
|
||||
if higressConfig == nil {
|
||||
return nil
|
||||
}
|
||||
if higressConfig.McpServer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validMcpServer(higressConfig.McpServer)
|
||||
}
|
||||
|
||||
func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHandler) {
|
||||
m.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
configs := make([]*config.Config, 0)
|
||||
mcpServer := m.GetMcpServer()
|
||||
namespace := m.Namespace
|
||||
|
||||
if mcpServer == nil || !mcpServer.Enable {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
mcpStruct := m.constructMcpServerStruct(mcpServer)
|
||||
if mcpStruct == "" {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
config := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.cors",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
|
||||
Value: util.BuildPatchStruct(mcpStruct),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
// Build servers configuration
|
||||
servers := "[]"
|
||||
if len(mcp.Servers) > 0 {
|
||||
serverConfigs := make([]string, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
serverConfig := fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"path": "%s",
|
||||
"type": "%s"`,
|
||||
server.Name, server.Path, server.Type)
|
||||
|
||||
if len(server.Config) > 0 {
|
||||
config, _ := json.Marshal(server.Config)
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"config": %s`, string(config))
|
||||
}
|
||||
|
||||
serverConfig += "}"
|
||||
serverConfigs[i] = serverConfig
|
||||
}
|
||||
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
|
||||
}
|
||||
|
||||
// Build match_list configuration
|
||||
matchList := "[]"
|
||||
if len(mcp.MatchList) > 0 {
|
||||
matchConfigs := make([]string, len(mcp.MatchList))
|
||||
for i, rule := range mcp.MatchList {
|
||||
matchConfigs[i] = fmt.Sprintf(`{
|
||||
"match_rule_domain": "%s",
|
||||
"match_rule_path": "%s",
|
||||
"match_rule_type": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType)
|
||||
}
|
||||
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
}
|
||||
|
||||
// 构建 Redis 配置
|
||||
redisConfig := "null"
|
||||
if mcp.Redis != nil {
|
||||
redisConfig = fmt.Sprintf(`{
|
||||
"address": "%s",
|
||||
"username": "%s",
|
||||
"password": "%s",
|
||||
"db": %d
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
|
||||
}
|
||||
|
||||
// 构建限流配置
|
||||
rateLimitConfig := "null"
|
||||
if mcp.Ratelimit != nil {
|
||||
whiteList := "[]"
|
||||
if len(mcp.Ratelimit.WhiteList) > 0 {
|
||||
whiteList = fmt.Sprintf(`["%s"]`, strings.Join(mcp.Ratelimit.WhiteList, `","`))
|
||||
}
|
||||
rateLimitConfig = fmt.Sprintf(`{
|
||||
"limit": %d,
|
||||
"window": %d,
|
||||
"white_list": %s
|
||||
}`, mcp.Ratelimit.Limit, mcp.Ratelimit.Window, whiteList)
|
||||
}
|
||||
|
||||
// Build complete configuration structure
|
||||
return fmt.Sprintf(`{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": %s,
|
||||
"rate_limit": %s,
|
||||
"sse_path_suffix": "%s",
|
||||
"match_list": %s,
|
||||
"servers": %s,
|
||||
"enable_user_level_server": %t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
redisConfig,
|
||||
rateLimitConfig,
|
||||
mcp.SsePathSuffix,
|
||||
matchList,
|
||||
servers,
|
||||
mcp.EnableUserLevelServer)
|
||||
}
|
||||
424
pkg/ingress/kube/configmap/mcp_server_test.go
Normal file
424
pkg/ingress/kube/configmap/mcp_server_test.go
Normal file
@@ -0,0 +1,424 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_validMcpServer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcp *McpServer
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
mcp: &McpServer{
|
||||
Enable: false,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "nil",
|
||||
mcp: nil,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "enabled but no redis config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "enabled with user level server but no redis config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: true,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
|
||||
},
|
||||
{
|
||||
name: "valid config with redis",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "*",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{
|
||||
{
|
||||
Name: "test-server",
|
||||
Path: "/test",
|
||||
Type: "test",
|
||||
Config: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validMcpServer(tt.mcp)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_compareMcpServer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
old *McpServer
|
||||
new *McpServer
|
||||
wantResult Result
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "compare both nil",
|
||||
old: nil,
|
||||
new: nil,
|
||||
wantResult: ResultNothing,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "compare result delete",
|
||||
old: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
new: nil,
|
||||
wantResult: ResultDelete,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "compare result equal",
|
||||
old: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
new: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantResult: ResultNothing,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "compare result replace",
|
||||
old: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
new: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "redis:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/test",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantResult: ResultReplace,
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := compareMcpServer(tt.old, tt.new)
|
||||
assert.Equal(t, tt.wantResult, result)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deepCopyMcpServer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcp *McpServer
|
||||
wantMcp *McpServer
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "deep copy with redis only",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantMcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "deep copy with full config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "*",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{
|
||||
{
|
||||
Name: "test-server",
|
||||
Path: "/test",
|
||||
Type: "test",
|
||||
Config: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantMcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "*",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{
|
||||
{
|
||||
Name: "test-server",
|
||||
Path: "/test",
|
||||
Type: "test",
|
||||
Config: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mcp, err := deepCopyMcpServer(tt.mcp)
|
||||
assert.Equal(t, tt.wantMcp, mcp)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_AddOrUpdateHigressConfig(t *testing.T) {
|
||||
eventPush := "default"
|
||||
defaultHandler := func(name string) {
|
||||
eventPush = "push"
|
||||
}
|
||||
|
||||
defaultName := util.ClusterNamespacedName{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
old *HigressConfig
|
||||
new *HigressConfig
|
||||
wantErr error
|
||||
wantEventPush string
|
||||
wantMcp *McpServer
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
old: &HigressConfig{
|
||||
McpServer: NewDefaultMcpServer(),
|
||||
},
|
||||
new: &HigressConfig{
|
||||
McpServer: NewDefaultMcpServer(),
|
||||
},
|
||||
wantErr: nil,
|
||||
wantEventPush: "default",
|
||||
wantMcp: NewDefaultMcpServer(),
|
||||
},
|
||||
{
|
||||
name: "replace and push - enable mcp server",
|
||||
old: &HigressConfig{
|
||||
McpServer: NewDefaultMcpServer(),
|
||||
},
|
||||
new: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
MatchList: []*MatchRule{},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
wantEventPush: "push",
|
||||
wantMcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
MatchList: []*MatchRule{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "replace and push - update config",
|
||||
old: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
MatchList: []*MatchRule{},
|
||||
},
|
||||
},
|
||||
new: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "redis:6379",
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
MatchList: []*MatchRule{},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
wantEventPush: "push",
|
||||
wantMcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "redis:6379",
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
MatchList: []*MatchRule{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete and push",
|
||||
old: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
MatchList: []*MatchRule{},
|
||||
},
|
||||
},
|
||||
new: &HigressConfig{
|
||||
McpServer: nil,
|
||||
},
|
||||
wantErr: nil,
|
||||
wantEventPush: "push",
|
||||
wantMcp: NewDefaultMcpServer(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("higress-system")
|
||||
m.eventHandler = defaultHandler
|
||||
eventPush = "default"
|
||||
err := m.AddOrUpdateHigressConfig(defaultName, tt.old, tt.new)
|
||||
assert.Equal(t, tt.wantEventPush, eventPush)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
assert.Equal(t, tt.wantMcp, m.GetMcpServer())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,21 +15,33 @@
|
||||
package http2rpc
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
v1 "github.com/alibaba/higress/client/pkg/apis/networking/v1"
|
||||
"github.com/alibaba/higress/client/pkg/clientset/versioned"
|
||||
informersv1 "github.com/alibaba/higress/client/pkg/informers/externalversions/networking/v1"
|
||||
listersv1 "github.com/alibaba/higress/client/pkg/listers/networking/v1"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/controller"
|
||||
kubeclient "github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
type Http2RpcController controller.Controller[listersv1.Http2RpcLister]
|
||||
|
||||
func NewController(client kubeclient.Client, clusterId cluster.ID) Http2RpcController {
|
||||
informer := client.HigressInformer().Networking().V1().Http2Rpcs().Informer()
|
||||
return controller.NewCommonController("http2rpc", client.HigressInformer().Networking().V1().Http2Rpcs().Lister(),
|
||||
informer, GetHttp2Rpc, clusterId)
|
||||
func NewController(client kubeclient.Client, options common.Options) Http2RpcController {
|
||||
var informer cache.SharedIndexInformer
|
||||
if options.WatchNamespace == "" {
|
||||
informer = client.HigressInformer().Networking().V1().Http2Rpcs().Informer()
|
||||
} else {
|
||||
informer = client.HigressInformer().InformerFor(&v1.Http2Rpc{}, func(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
|
||||
return informersv1.NewHttp2RpcInformer(client, options.WatchNamespace, resyncPeriod, nil)
|
||||
})
|
||||
}
|
||||
return controller.NewCommonController("http2rpc", listersv1.NewHttp2RpcLister(informer.GetIndexer()), informer, GetHttp2Rpc, options.ClusterId)
|
||||
}
|
||||
|
||||
func GetHttp2Rpc(lister listersv1.Http2RpcLister, namespacedName types.NamespacedName) (controllers.Object, error) {
|
||||
|
||||
@@ -100,7 +100,7 @@ type controller struct {
|
||||
// NewController creates a new Kubernetes controller
|
||||
func NewController(localKubeClient, client kubeclient.Client, options common.Options,
|
||||
secretController secret.SecretController) common.IngressController {
|
||||
opts := ktypes.InformerOptions{}
|
||||
opts := ktypes.InformerOptions{Namespace: options.WatchNamespace}
|
||||
ingressInformer := util.GetInformerFiltered(client, opts, gvrIngressV1Beta1, &ingress.Ingress{},
|
||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
return client.Kube().NetworkingV1beta1().Ingresses(opts.Namespace).List(context.Background(), options)
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestIngressControllerApplies(t *testing.T) {
|
||||
|
||||
options := common.Options{IngressClass: "mse", ClusterId: ""}
|
||||
|
||||
secretController := secret.NewController(localKubeClient, options.ClusterId)
|
||||
secretController := secret.NewController(localKubeClient, options)
|
||||
ingressController := NewController(localKubeClient, client, options, secretController)
|
||||
|
||||
testcases := map[string]func(*testing.T, common.IngressController){
|
||||
@@ -253,7 +253,7 @@ func TestIngressControllerConventions(t *testing.T) {
|
||||
|
||||
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
|
||||
|
||||
secretController := secret.NewController(localKubeClient, options.ClusterId)
|
||||
secretController := secret.NewController(localKubeClient, options)
|
||||
ingressController := NewController(localKubeClient, client, options, secretController)
|
||||
|
||||
testcases := map[string]func(*testing.T, common.IngressController){
|
||||
@@ -1142,7 +1142,7 @@ func TestIngressControllerProcessing(t *testing.T) {
|
||||
|
||||
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
|
||||
|
||||
secretController := secret.NewController(localKubeClient, options.ClusterId)
|
||||
secretController := secret.NewController(localKubeClient, options)
|
||||
|
||||
opts := ktypes.InformerOptions{}
|
||||
ingressInformer := util.GetInformerFiltered(fakeClient, opts, gvrIngressV1Beta1, &ingress.Ingress{},
|
||||
|
||||
@@ -92,7 +92,7 @@ type controller struct {
|
||||
|
||||
// NewController creates a new Kubernetes controller
|
||||
func NewController(localKubeClient, client kubeclient.Client, options common.Options, secretController secret.SecretController) common.IngressController {
|
||||
opts := ktypes.InformerOptions{}
|
||||
opts := ktypes.InformerOptions{Namespace: options.WatchNamespace}
|
||||
ingressInformer := schemakubeclient.GetInformerFilteredFromGVR(client, opts, gvr.Ingress)
|
||||
ingressLister := networkinglister.NewIngressLister(ingressInformer.Informer.GetIndexer())
|
||||
serviceInformer := schemakubeclient.GetInformerFilteredFromGVR(client, opts, gvr.Service)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
@@ -43,7 +44,9 @@ import (
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
networkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
|
||||
"knative.dev/networking/pkg/client/clientset/versioned"
|
||||
informernetworkingv1alpha1 "knative.dev/networking/pkg/client/informers/externalversions/networking/v1alpha1"
|
||||
listernetworkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
@@ -76,7 +79,7 @@ type controller struct {
|
||||
ingresses map[string]*ingress.Ingress
|
||||
|
||||
ingressInformer cache.SharedInformer
|
||||
ingressLister networkingv1alpha1.IngressLister
|
||||
ingressLister listernetworkingv1alpha1.IngressLister
|
||||
serviceInformer informerfactory.StartableInformer
|
||||
serviceLister listerv1.ServiceLister
|
||||
secretController secret.SecretController
|
||||
@@ -86,16 +89,23 @@ type controller struct {
|
||||
// NewController creates a new Kubernetes controller
|
||||
func NewController(localKubeClient, client kube.Client, options common.Options,
|
||||
secretController secret.SecretController) common.KIngressController {
|
||||
//var namespace string = "default"
|
||||
ingressInformer := client.KIngressInformer().Networking().V1alpha1().Ingresses()
|
||||
serviceInformer := schemakubeclient.GetInformerFilteredFromGVR(client, ktypes.InformerOptions{}, gvr.Service)
|
||||
var ingressInformer cache.SharedIndexInformer
|
||||
if options.WatchNamespace == "" {
|
||||
ingressInformer = client.KIngressInformer().Networking().V1alpha1().Ingresses().Informer()
|
||||
} else {
|
||||
ingressInformer = client.KIngressInformer().InformerFor(&ingress.Ingress{}, func(c versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
|
||||
return informernetworkingv1alpha1.NewIngressInformer(c, options.WatchNamespace, resyncPeriod, nil)
|
||||
})
|
||||
}
|
||||
ingressLister := listernetworkingv1alpha1.NewIngressLister(ingressInformer.GetIndexer())
|
||||
serviceInformer := schemakubeclient.GetInformerFilteredFromGVR(client, ktypes.InformerOptions{Namespace: options.WatchNamespace}, gvr.Service)
|
||||
serviceLister := listerv1.NewServiceLister(serviceInformer.Informer.GetIndexer())
|
||||
|
||||
c := &controller{
|
||||
options: options,
|
||||
ingresses: make(map[string]*ingress.Ingress),
|
||||
ingressInformer: ingressInformer.Informer(),
|
||||
ingressLister: ingressInformer.Lister(),
|
||||
ingressInformer: ingressInformer,
|
||||
ingressLister: ingressLister,
|
||||
serviceInformer: serviceInformer,
|
||||
serviceLister: serviceLister,
|
||||
secretController: secretController,
|
||||
|
||||
@@ -154,7 +154,7 @@ func TestKIngressControllerConventions(t *testing.T) {
|
||||
|
||||
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
|
||||
|
||||
secretController := secret.NewController(localKubeClient, options.ClusterId)
|
||||
secretController := secret.NewController(localKubeClient, options)
|
||||
ingressController := NewController(localKubeClient, client, options, secretController)
|
||||
|
||||
testcases := map[string]func(*testing.T, common.KIngressController){
|
||||
|
||||
@@ -15,21 +15,33 @@
|
||||
package mcpbridge
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
v1 "github.com/alibaba/higress/client/pkg/apis/networking/v1"
|
||||
"github.com/alibaba/higress/client/pkg/clientset/versioned"
|
||||
informersv1 "github.com/alibaba/higress/client/pkg/informers/externalversions/networking/v1"
|
||||
listersv1 "github.com/alibaba/higress/client/pkg/listers/networking/v1"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/controller"
|
||||
kubeclient "github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
type McpBridgeController controller.Controller[listersv1.McpBridgeLister]
|
||||
|
||||
func NewController(client kubeclient.Client, clusterId cluster.ID) McpBridgeController {
|
||||
informer := client.HigressInformer().Networking().V1().McpBridges().Informer()
|
||||
return controller.NewCommonController("mcpbridge", client.HigressInformer().Networking().V1().McpBridges().Lister(),
|
||||
informer, GetMcpBridge, clusterId)
|
||||
func NewController(client kubeclient.Client, options common.Options) McpBridgeController {
|
||||
var informer cache.SharedIndexInformer
|
||||
if options.WatchNamespace == "" {
|
||||
informer = client.HigressInformer().Networking().V1().McpBridges().Informer()
|
||||
} else {
|
||||
informer = client.HigressInformer().InformerFor(&v1.McpBridge{}, func(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
|
||||
return informersv1.NewMcpBridgeInformer(client, options.WatchNamespace, resyncPeriod, nil)
|
||||
})
|
||||
}
|
||||
return controller.NewCommonController("mcpbridge", listersv1.NewMcpBridgeLister(informer.GetIndexer()), informer, GetMcpBridge, options.ClusterId)
|
||||
}
|
||||
|
||||
func GetMcpBridge(lister listersv1.McpBridgeLister, namespacedName types.NamespacedName) (controllers.Object, error) {
|
||||
|
||||
@@ -15,15 +15,14 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/controller"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config/schema/gvr"
|
||||
schemakubeclient "istio.io/istio/pkg/config/schema/kubeclient"
|
||||
kubeclient "istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
ktypes "istio.io/istio/pkg/kube/kubetypes"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
@@ -31,17 +30,17 @@ import (
|
||||
|
||||
type SecretController controller.Controller[listersv1.SecretLister]
|
||||
|
||||
func NewController(client kubeclient.Client, clusterId cluster.ID) SecretController {
|
||||
func NewController(client kubeclient.Client, options common.Options) SecretController {
|
||||
opts := ktypes.InformerOptions{
|
||||
Namespace: metav1.NamespaceAll,
|
||||
Cluster: clusterId,
|
||||
Namespace: options.WatchNamespace,
|
||||
Cluster: options.ClusterId,
|
||||
FieldSelector: fields.AndSelectors(
|
||||
fields.OneTermNotEqualSelector("type", "helm.sh/release.v1"),
|
||||
fields.OneTermNotEqualSelector("type", string(v1.SecretTypeServiceAccountToken)),
|
||||
).String(),
|
||||
}
|
||||
informer := schemakubeclient.GetInformerFilteredFromGVR(client, opts, gvr.Secret)
|
||||
return controller.NewCommonController("secret", listersv1.NewSecretLister(informer.Informer.GetIndexer()), informer.Informer, GetSecret, clusterId)
|
||||
return controller.NewCommonController("secret", listersv1.NewSecretLister(informer.Informer.GetIndexer()), informer.Informer, GetSecret, options.ClusterId)
|
||||
}
|
||||
|
||||
func GetSecret(lister listersv1.SecretLister, namespacedName types.NamespacedName) (controllers.Object, error) {
|
||||
|
||||
@@ -16,6 +16,7 @@ package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -43,7 +44,7 @@ var period = time.Second
|
||||
|
||||
func TestController(t *testing.T) {
|
||||
client := kubeclient.NewFakeClient()
|
||||
ctrl := NewController(client, "fake-cluster")
|
||||
ctrl := NewController(client, common.Options{ClusterId: "fake-cluster"})
|
||||
|
||||
stop := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -15,21 +15,33 @@
|
||||
package wasmplugin
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
v1 "github.com/alibaba/higress/client/pkg/apis/extensions/v1alpha1"
|
||||
"github.com/alibaba/higress/client/pkg/clientset/versioned"
|
||||
informersv1 "github.com/alibaba/higress/client/pkg/informers/externalversions/extensions/v1alpha1"
|
||||
listersv1 "github.com/alibaba/higress/client/pkg/listers/extensions/v1alpha1"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/controller"
|
||||
kubeclient "github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
type WasmPluginController controller.Controller[listersv1.WasmPluginLister]
|
||||
|
||||
func NewController(client kubeclient.Client, clusterId cluster.ID) WasmPluginController {
|
||||
informer := client.HigressInformer().Extensions().V1alpha1().WasmPlugins().Informer()
|
||||
return controller.NewCommonController("wasmplugin", client.HigressInformer().Extensions().V1alpha1().WasmPlugins().Lister(),
|
||||
informer, GetWasmPlugin, clusterId)
|
||||
func NewController(client kubeclient.Client, options common.Options) WasmPluginController {
|
||||
var informer cache.SharedIndexInformer
|
||||
if options.WatchNamespace == "" {
|
||||
informer = client.HigressInformer().Extensions().V1alpha1().WasmPlugins().Informer()
|
||||
} else {
|
||||
informer = client.HigressInformer().InformerFor(&v1.WasmPlugin{}, func(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
|
||||
return informersv1.NewWasmPluginInformer(client, options.WatchNamespace, resyncPeriod, nil)
|
||||
})
|
||||
}
|
||||
return controller.NewCommonController("wasmplugin", listersv1.NewWasmPluginLister(informer.GetIndexer()), informer, GetWasmPlugin, options.ClusterId)
|
||||
}
|
||||
|
||||
func GetWasmPlugin(lister listersv1.WasmPluginLister, namespacedName types.NamespacedName) (controllers.Object, error) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
istiomodel "istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
@@ -45,13 +44,13 @@ type IngressTranslation struct {
|
||||
higressDomainCache model.IngressDomainCollection
|
||||
}
|
||||
|
||||
func NewIngressTranslation(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, clusterId cluster.ID) *IngressTranslation {
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
func NewIngressTranslation(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *IngressTranslation {
|
||||
if options.ClusterId == "Kubernetes" {
|
||||
options.ClusterId = ""
|
||||
}
|
||||
Config := &IngressTranslation{
|
||||
ingressConfig: ingressconfig.NewIngressConfig(localKubeClient, xdsUpdater, namespace, clusterId),
|
||||
kingressConfig: ingressconfig.NewKIngressConfig(localKubeClient, xdsUpdater, namespace, clusterId),
|
||||
ingressConfig: ingressconfig.NewIngressConfig(localKubeClient, xdsUpdater, namespace, options),
|
||||
kingressConfig: ingressconfig.NewKIngressConfig(localKubeClient, xdsUpdater, namespace, options),
|
||||
}
|
||||
return Config
|
||||
}
|
||||
|
||||
39
plugins/golang-filter/Dockerfile
Normal file
39
plugins/golang-filter/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
FROM golang:1.23-bullseye AS golang-base
|
||||
|
||||
ARG GOPROXY
|
||||
ARG GO_FILTER_NAME
|
||||
ARG GOARCH
|
||||
|
||||
ENV GOFLAGS=-buildvcs=false
|
||||
ENV GOPROXY=${GOPROXY}
|
||||
ENV GOARCH=${GOARCH}
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
# 根据目标架构安装对应的编译工具
|
||||
RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
echo "Installing ARM64 toolchain" && \
|
||||
apt-get update && \
|
||||
apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu; \
|
||||
else \
|
||||
echo "Installing AMD64 toolchain" && \
|
||||
apt-get update && \
|
||||
apt-get install -y gcc binutils; \
|
||||
fi
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR /workspace/$GO_FILTER_NAME
|
||||
|
||||
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 .; \
|
||||
fi
|
||||
|
||||
FROM scratch AS output
|
||||
ARG GO_FILTER_NAME
|
||||
ARG GOARCH
|
||||
COPY --from=golang-base /${GO_FILTER_NAME}.so golang-filter_${GOARCH}.so
|
||||
12
plugins/golang-filter/Makefile
Normal file
12
plugins/golang-filter/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
GO_FILTER_NAME ?= mcp-server
|
||||
GOPROXY := $(shell go env GOPROXY)
|
||||
GOARCH ?= amd64
|
||||
|
||||
.DEFAULT:
|
||||
build:
|
||||
DOCKER_BUILDKIT=1 docker build --build-arg GOPROXY=$(GOPROXY) \
|
||||
--build-arg GO_FILTER_NAME=${GO_FILTER_NAME} \
|
||||
--build-arg GOARCH=${GOARCH} \
|
||||
-t ${GO_FILTER_NAME} \
|
||||
--output ./${GO_FILTER_NAME} \
|
||||
.
|
||||
47
plugins/golang-filter/README.md
Normal file
47
plugins/golang-filter/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Golang HTTP Filter
|
||||
|
||||
[English](./README_en.md) | 简体中文
|
||||
|
||||
## 简介
|
||||
|
||||
Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filter。该框架支持在请求和响应流程中执行 Golang 代码,使 Envoy 的扩展开发变得更加简单。最重要的是,使用此框架开发的 Go 插件可以独立于 Envoy 进行编译,这大大提高了开发和部署的灵活性。
|
||||
|
||||
> **注意** Golang Filter 需要 Higress 2.1.0 或更高版本才能使用。
|
||||
## 特性
|
||||
|
||||
- 支持在HTTP请求和响应流程中执行 Go 代码
|
||||
- 支持插件独立编译,无需重新编译 Envoy
|
||||
- 提供简洁的 API 接口
|
||||
- 支持请求/响应头部修改
|
||||
- 支持请求/响应体修改
|
||||
- 支持同步请求
|
||||
|
||||
## 快速开始
|
||||
|
||||
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
|
||||
|
||||
## 配置示例
|
||||
|
||||
```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
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
your_config_here: value
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 快速构建
|
||||
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
```
|
||||
45
plugins/golang-filter/README_en.md
Normal file
45
plugins/golang-filter/README_en.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Golang HTTP Filter
|
||||
|
||||
English | [简体中文](./README.md)
|
||||
|
||||
## Introduction
|
||||
|
||||
The Golang HTTP Filter allows developers to write custom Envoy Filters using the Go language. This framework supports executing Golang code during both request and response flows, making it easier to extend Envoy. Most importantly, Go plugins developed using this framework can be compiled independently of Envoy, which greatly enhances development and deployment flexibility.
|
||||
|
||||
> **注意** Golang Filter require Higress version 2.1.0 or higher to be used.
|
||||
## Features
|
||||
|
||||
- Support for Golang code execution in both request and response flows
|
||||
- Independent plugin compilation without rebuilding Envoy
|
||||
- Simple and clean API interface
|
||||
- Request/response header modification
|
||||
- Request/response body modification
|
||||
- Synchronous request support
|
||||
|
||||
## Quick Start
|
||||
|
||||
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.
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```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
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
your_config_here: value
|
||||
```
|
||||
|
||||
## Quick Build
|
||||
|
||||
Use the following command to quickly build the golang filter plugin:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
```
|
||||
65
plugins/golang-filter/mcp-server/README.md
Normal file
65
plugins/golang-filter/mcp-server/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# MCP Server
|
||||
[English](./README_en.md) | 简体中文
|
||||
|
||||
## 概述
|
||||
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,用于实现服务器端事件(SSE)和消息通信功能。该插件支持多种数据库类型,并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
|
||||
|
||||
> **注意**:MCP Server需要 Higress 2.1.0 或更高版本才能使用。
|
||||
## 项目结构
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # 配置解析相关代码
|
||||
├── filter.go # 请求处理相关代码
|
||||
├── internal/ # 内部实现逻辑
|
||||
├── servers/ # MCP 服务器实现
|
||||
├── go.mod # Go模块依赖定义
|
||||
└── go.sum # Go模块依赖校验
|
||||
```
|
||||
## MCP Server开发指南
|
||||
|
||||
```go
|
||||
// 在init函数中注册你的服务器
|
||||
// 参数1: 服务器名称
|
||||
// 参数2: 配置结构体实例
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// 服务器配置结构体
|
||||
type DemoConfig struct {
|
||||
helloworld string
|
||||
}
|
||||
|
||||
// 解析配置方法
|
||||
// 从配置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
|
||||
}
|
||||
|
||||
// 创建新的MCP服务器实例
|
||||
// serverName: 服务器名称
|
||||
// 返回值: MCP服务器实例和可能的错误
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
|
||||
// 添加工具方法到服务器
|
||||
// mcpServer.AddTool()
|
||||
|
||||
// 添加资源到服务器
|
||||
// mcpServer.AddResource()
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**:
|
||||
需要在config.go里面使用下划线导入以执行包的init函数
|
||||
```go
|
||||
import (
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
)
|
||||
```
|
||||
67
plugins/golang-filter/mcp-server/README_en.md
Normal file
67
plugins/golang-filter/mcp-server/README_en.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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"
|
||||
)
|
||||
```
|
||||
235
plugins/golang-filter/mcp-server/config.go
Normal file
235
plugins/golang-filter/mcp-server/config.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
|
||||
)
|
||||
|
||||
const Name = "mcp-session"
|
||||
const Version = "1.0.0"
|
||||
const DefaultServerName = "defaultServer"
|
||||
const ConfigPathSuffix = "/config"
|
||||
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("PProf server recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
|
||||
}()
|
||||
}
|
||||
|
||||
type config struct {
|
||||
ssePathSuffix string
|
||||
redisClient *internal.RedisClient
|
||||
servers []*internal.SSEServer
|
||||
defaultServer *internal.SSEServer
|
||||
matchList []internal.MatchRule
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
if c.redisClient != nil {
|
||||
api.LogDebug("Closing Redis client")
|
||||
c.redisClient.Close()
|
||||
}
|
||||
for _, server := range c.servers {
|
||||
server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
}
|
||||
|
||||
// Parse the filter configuration
|
||||
func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
configStruct := &xds.TypedStruct{}
|
||||
if err := any.UnmarshalTo(configStruct); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := configStruct.Value
|
||||
|
||||
conf := &config{
|
||||
matchList: make([]internal.MatchRule, 0),
|
||||
servers: make([]*internal.SSEServer, 0),
|
||||
}
|
||||
|
||||
// Parse match_list if exists
|
||||
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
|
||||
for _, item := range matchList {
|
||||
if ruleMap, ok := item.(map[string]interface{}); ok {
|
||||
rule := internal.MatchRule{}
|
||||
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
|
||||
rule.MatchRuleDomain = domain
|
||||
}
|
||||
if path, ok := ruleMap["match_rule_path"].(string); ok {
|
||||
rule.MatchRulePath = path
|
||||
}
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = internal.RuleType(ruleType)
|
||||
}
|
||||
conf.matchList = append(conf.matchList, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redis configuration is optional
|
||||
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
|
||||
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
}
|
||||
|
||||
redisClient, err := internal.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
api.LogDebug("Redis client initialized")
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
enableUserLevelServer = false
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
conf.enableUserLevelServer = enableUserLevelServer
|
||||
|
||||
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
|
||||
rateLimitConfig := &handler.MCPRatelimitConfig{}
|
||||
if limit, ok := rateLimit["limit"].(float64); ok {
|
||||
rateLimitConfig.Limit = int(limit)
|
||||
}
|
||||
if window, ok := rateLimit["window"].(float64); ok {
|
||||
rateLimitConfig.Window = int(window)
|
||||
}
|
||||
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
|
||||
for _, item := range whiteList {
|
||||
if uid, ok := item.(string); ok {
|
||||
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorText, ok := rateLimit["error_text"].(string); ok {
|
||||
rateLimitConfig.ErrorText = errorText
|
||||
}
|
||||
conf.rateLimitConfig = rateLimitConfig
|
||||
}
|
||||
|
||||
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
|
||||
if !ok || ssePathSuffix == "" {
|
||||
return nil, fmt.Errorf("sse path suffix is not set or empty")
|
||||
}
|
||||
conf.ssePathSuffix = ssePathSuffix
|
||||
|
||||
serverConfigs, ok := v.AsMap()["servers"].([]interface{})
|
||||
if !ok {
|
||||
api.LogDebug("No servers are configured")
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
for _, serverConfig := range serverConfigs {
|
||||
serverConfigMap, ok := serverConfig.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server config must be an object")
|
||||
}
|
||||
serverType, ok := serverConfigMap["type"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server type is not set")
|
||||
}
|
||||
serverPath, ok := serverConfigMap["path"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server %s path is not set", serverType)
|
||||
}
|
||||
serverName, ok := serverConfigMap["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server %s name is not set", serverType)
|
||||
}
|
||||
server := internal.GlobalRegistry.GetServer(serverType)
|
||||
|
||||
if server == nil {
|
||||
return nil, fmt.Errorf("server %s is not registered", serverType)
|
||||
}
|
||||
serverConfig, ok := serverConfigMap["config"].(map[string]interface{})
|
||||
if !ok {
|
||||
api.LogDebug(fmt.Sprintf("No config provided for server %s", serverType))
|
||||
}
|
||||
api.LogDebug(fmt.Sprintf("Server config: %+v", serverConfig))
|
||||
|
||||
err := server.ParseConfig(serverConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse server config: %w", err)
|
||||
}
|
||||
|
||||
serverInstance, err := server.NewServer(serverName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
|
||||
internal.WithRedisClient(conf.redisClient),
|
||||
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
|
||||
internal.WithMessageEndpoint(serverPath)))
|
||||
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
parentConfig := parent.(*config)
|
||||
childConfig := child.(*config)
|
||||
|
||||
newConfig := *parentConfig
|
||||
if childConfig.redisClient != nil {
|
||||
newConfig.redisClient = childConfig.redisClient
|
||||
}
|
||||
if childConfig.ssePathSuffix != "" {
|
||||
newConfig.ssePathSuffix = childConfig.ssePathSuffix
|
||||
}
|
||||
if childConfig.servers != nil {
|
||||
newConfig.servers = childConfig.servers
|
||||
}
|
||||
if childConfig.defaultServer != nil {
|
||||
newConfig.defaultServer = childConfig.defaultServer
|
||||
}
|
||||
if childConfig.matchList != nil {
|
||||
newConfig.matchList = childConfig.matchList
|
||||
}
|
||||
return &newConfig
|
||||
}
|
||||
|
||||
func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
conf, ok := c.(*config)
|
||||
if !ok {
|
||||
panic("unexpected config type")
|
||||
}
|
||||
return &filter{
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
321
plugins/golang-filter/mcp-server/filter.go
Normal file
321
plugins/golang-filter/mcp-server/filter.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
|
||||
)
|
||||
|
||||
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
|
||||
// Because api.PassThroughStreamFilter provides a default implementation.
|
||||
type filter struct {
|
||||
api.PassThroughStreamFilter
|
||||
|
||||
callbacks api.FilterCallbackHandler
|
||||
path string
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
message bool
|
||||
proxyURL *url.URL
|
||||
skip bool
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
ratelimit bool
|
||||
mcpRatelimitHandler *handler.MCPRatelimitHandler
|
||||
}
|
||||
|
||||
type RequestURL struct {
|
||||
method string
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
baseURL string
|
||||
parsedURL *url.URL
|
||||
internalIP bool
|
||||
}
|
||||
|
||||
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
method, _ := header.Get(":method")
|
||||
scheme, _ := header.Get(":scheme")
|
||||
host, _ := header.Get(":authority")
|
||||
path, _ := header.Get(":path")
|
||||
internalIP, _ := header.Get("x-envoy-internal")
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
parsedURL, _ := url.Parse(path)
|
||||
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
|
||||
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL, internalIP: internalIP == "true"}
|
||||
}
|
||||
|
||||
// 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 := NewRequestURL(header)
|
||||
f.path = url.parsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
if !internal.IsMatch(f.config.matchList, url.host, f.path) {
|
||||
f.skip = true
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", url.parsedURL.String())
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetSSEEndpoint() {
|
||||
if url.method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.serverName = server.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
api.LogDebugf("%s SSE connection started", server.GetServerName())
|
||||
return api.LocalReply
|
||||
} else if f.path == server.GetMessageEndpoint() {
|
||||
if url.method != http.MethodPost {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
// Create a new http.Request object
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.parsedURL,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
api.LogDebugf("Message request: %v", url.parsedURL)
|
||||
// Copy headers from api.RequestHeaderMap to http.Header
|
||||
header.Range(func(key, value string) bool {
|
||||
f.req.Header.Add(key, value)
|
||||
return true
|
||||
})
|
||||
f.message = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.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())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
}
|
||||
f.userLevelConfig = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
|
||||
f.proxyURL = url.parsedURL
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.parsedURL.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
// Get encoded config
|
||||
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if encodedConfig != "" {
|
||||
header.Set("x-higress-mcpserver-config", encodedConfig)
|
||||
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
|
||||
}
|
||||
}
|
||||
f.ratelimit = true
|
||||
}
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if url.method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.config.defaultServer = internal.NewSSEServer(internal.NewMCPServer(DefaultServerName, Version),
|
||||
internal.WithSSEEndpoint(f.config.ssePathSuffix),
|
||||
internal.WithMessageEndpoint(strings.TrimSuffix(url.parsedURL.Path, f.config.ssePathSuffix)),
|
||||
internal.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
return api.LocalReply
|
||||
}
|
||||
|
||||
// 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.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.message {
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetMessageEndpoint() {
|
||||
// Create a response recorder to capture the response
|
||||
recorder := httptest.NewRecorder()
|
||||
// Call the handleMessage method of SSEServer with complete body
|
||||
httpStatus := server.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
f.message = false
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(httpStatus, recorder.Body.String(), recorder.Header(), 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
} else if f.userLevelConfig {
|
||||
// Handle config POST request
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
|
||||
return api.LocalReply
|
||||
} else if f.ratelimit {
|
||||
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
|
||||
api.LogDebugf("Not a tools call request, skipping ratelimit")
|
||||
return api.Continue
|
||||
}
|
||||
parts := strings.Split(f.req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
api.LogWarnf("Access denied: no valid uid found")
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
|
||||
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
|
||||
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// 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.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
} else {
|
||||
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// 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.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := internal.GetSSEChannelName(sessionID)
|
||||
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
|
||||
publishErr := f.config.redisClient.Publish(channel, eventData)
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
// handle specific server
|
||||
for _, server := range f.config.servers {
|
||||
if f.serverName == server.GetServerName() {
|
||||
buffer.Reset()
|
||||
server.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
}
|
||||
// handle default server
|
||||
if f.serverName == f.config.defaultServer.GetServerName() {
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
return api.Continue
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
if f.serverName != "" && f.stopChan != nil {
|
||||
select {
|
||||
case <-f.stopChan:
|
||||
return
|
||||
default:
|
||||
api.LogDebug("Stopping SSE connection")
|
||||
close(f.stopChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the request is a tools/call request
|
||||
func checkJSONRPCMethod(body []byte, method string) bool {
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(body, &request); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return request.Method == method
|
||||
}
|
||||
104
plugins/golang-filter/mcp-server/go.mod
Normal file
104
plugins/golang-filter/mcp-server/go.mod
Normal file
@@ -0,0 +1,104 @@
|
||||
module github.com/alibaba/higress/plugins/golang-filter/mcp-server
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mark3labs/mcp-go v0.12.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gorm.io/driver/clickhouse v0.6.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea v1.2.2 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.15.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.61.5 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.2.9
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40
|
||||
790
plugins/golang-filter/mcp-server/go.sum
Normal file
790
plugins/golang-filter/mcp-server/go.sum
Normal file
@@ -0,0 +1,790 @@
|
||||
cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w=
|
||||
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
|
||||
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 h1:+DAKPMnxLS7pduQZsrJc8OhdLS2L9MfDEJ2TS+hpYDM=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.23.2/go.mod h1:aNap51J1OM3yxQJRgM+AlP/MPkGBCL8A74uQThoQhR0=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw=
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
|
||||
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU=
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg=
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI=
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU=
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 h1:8S0mtD101RDYa0LXwdoqgN0RxdMmmJYjq8g2mk7/lQ4=
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5/go.mod h1:M19fxYz3gpm0ETnoKweYyYtqrtnVtrpKFpwsghbw+cQ=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY=
|
||||
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99 h1:jih/Ieb7BFgVCStgvY5fXQ3mI9ByOt4wfwUF0d7qmqI=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99/go.mod h1:x7d0dNbE0xGuDBUkBg19VGCgnPQ+lJ2k8lDzDzKExow=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/mark3labs/mcp-go v0.12.0 h1:Pue1Tdwqcz77GHq18uzgmLT3wmeDUxXUSAqSwhGLhVo=
|
||||
github.com/mark3labs/mcp-go v0.12.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
|
||||
github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/clickhouse v0.6.1 h1:t7JMB6sLBXxN8hEO6RdzCbJCwq/jAEVZdwXlmQs1Sd4=
|
||||
gorm.io/driver/clickhouse v0.6.1/go.mod h1:riMYpJcGZ3sJ/OAZZ1rEP1j/Y0H6cByOAnwz7fo2AyM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
162
plugins/golang-filter/mcp-server/handler/config_handler.go
Normal file
162
plugins/golang-filter/mcp-server/handler/config_handler.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// MCPConfigHandler handles configuration requests for MCP server
|
||||
type MCPConfigHandler struct {
|
||||
configStore ConfigStore
|
||||
callbacks api.FilterCallbackHandler
|
||||
}
|
||||
|
||||
// NewMCPConfigHandler creates a new instance of MCP configuration handler
|
||||
func NewMCPConfigHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
|
||||
return &MCPConfigHandler{
|
||||
configStore: NewRedisConfigStore(redisClient),
|
||||
callbacks: callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleConfigRequest processes configuration requests
|
||||
func (h *MCPConfigHandler) HandleConfigRequest(req *http.Request, body []byte) bool {
|
||||
// Check if it's a configuration request
|
||||
if !strings.HasSuffix(req.URL.Path, "/config") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract serverName and uid from path
|
||||
pathParts := strings.Split(strings.TrimSuffix(req.URL.Path, "/config"), "/")
|
||||
if len(pathParts) < 2 {
|
||||
h.sendErrorResponse(http.StatusBadRequest, "INVALID_PATH", "Invalid path format")
|
||||
return true
|
||||
}
|
||||
uid := pathParts[len(pathParts)-1]
|
||||
serverName := pathParts[len(pathParts)-2]
|
||||
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
return h.handleGetConfig(serverName, uid)
|
||||
case http.MethodPost:
|
||||
return h.handleStoreConfig(serverName, uid, body)
|
||||
default:
|
||||
h.sendErrorResponse(http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetConfig handles configuration retrieval requests
|
||||
func (h *MCPConfigHandler) handleGetConfig(serverName string, uid string) bool {
|
||||
config, err := h.configStore.GetConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to get config for server %s, uid %s: %v", serverName, uid, err)
|
||||
h.sendErrorResponse(http.StatusInternalServerError, "CONFIG_ERROR", fmt.Sprintf("Failed to get configuration: %s", err.Error()))
|
||||
return true
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Config map[string]string `json:"config"`
|
||||
}{
|
||||
Success: true,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
headers := map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
|
||||
http.StatusOK,
|
||||
string(responseBytes),
|
||||
headers, 0, "",
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// handleStoreConfig handles configuration storage requests
|
||||
func (h *MCPConfigHandler) handleStoreConfig(serverName string, uid string, body []byte) bool {
|
||||
// Parse request body
|
||||
var requestBody struct {
|
||||
Config map[string]string `json:"config"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &requestBody); err != nil {
|
||||
api.LogErrorf("Invalid request format for server %s, uid %s: %v", serverName, uid, err)
|
||||
h.sendErrorResponse(http.StatusBadRequest, "INVALID_REQUEST", fmt.Sprintf("Invalid request format: %s", err.Error()))
|
||||
return true
|
||||
}
|
||||
|
||||
if requestBody.Config == nil {
|
||||
h.sendErrorResponse(http.StatusBadRequest, "INVALID_REQUEST", "Config cannot be null")
|
||||
return true
|
||||
}
|
||||
|
||||
response, err := h.configStore.StoreConfig(serverName, uid, requestBody.Config)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to store config for server %s, uid %s: %v", serverName, uid, err)
|
||||
h.sendErrorResponse(http.StatusInternalServerError, "CONFIG_ERROR", fmt.Sprintf("Failed to store configuration: %s", err.Error()))
|
||||
return true
|
||||
}
|
||||
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
headers := map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
|
||||
http.StatusOK,
|
||||
string(responseBytes),
|
||||
headers, 0, "",
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// sendErrorResponse sends an error response with the specified status, code and message
|
||||
func (h *MCPConfigHandler) sendErrorResponse(status int, code string, message string) {
|
||||
response := &ConfigResponse{
|
||||
Success: false,
|
||||
Error: &struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
headers := map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
|
||||
status,
|
||||
string(responseBytes),
|
||||
headers, 0, "",
|
||||
)
|
||||
}
|
||||
|
||||
// GetEncodedConfig retrieves and encodes the configuration for a given server and uid
|
||||
func (h *MCPConfigHandler) GetEncodedConfig(serverName string, uid string) (string, error) {
|
||||
conf, err := h.configStore.GetConfig(serverName, uid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
// Check if config exists and is not empty
|
||||
if len(conf) > 0 {
|
||||
// Convert config map to JSON string
|
||||
configBytes, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
// Encode JSON string to base64
|
||||
return base64.StdEncoding.EncodeToString(configBytes), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
105
plugins/golang-filter/mcp-server/handler/config_store.go
Normal file
105
plugins/golang-filter/mcp-server/handler/config_store.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
configExpiry = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// GetConfigStoreKey returns the Redis channel name for the given session ID
|
||||
func GetConfigStoreKey(serverName string, uid string) string {
|
||||
return fmt.Sprintf("mcp-server-config:%s:%s", serverName, uid)
|
||||
}
|
||||
|
||||
// ConfigResponse represents the response structure for configuration operations
|
||||
type ConfigResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error *struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigStore defines the interface for configuration storage operations
|
||||
type ConfigStore interface {
|
||||
// StoreConfig stores user configuration
|
||||
StoreConfig(serverName string, uid string, config map[string]string) (*ConfigResponse, error)
|
||||
// GetConfig retrieves user configuration
|
||||
GetConfig(serverName string, uid string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// RedisConfigStore implements configuration storage using Redis
|
||||
type RedisConfigStore struct {
|
||||
redisClient *internal.RedisClient
|
||||
}
|
||||
|
||||
// NewRedisConfigStore creates a new instance of Redis configuration storage
|
||||
func NewRedisConfigStore(redisClient *internal.RedisClient) ConfigStore {
|
||||
return &RedisConfigStore{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreConfig stores configuration in Redis
|
||||
func (s *RedisConfigStore) StoreConfig(serverName string, uid string, config map[string]string) (*ConfigResponse, error) {
|
||||
key := GetConfigStoreKey(serverName, uid)
|
||||
|
||||
// Convert config to JSON
|
||||
configBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return &ConfigResponse{
|
||||
Success: false,
|
||||
Error: &struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Code: "MARSHAL_ERROR",
|
||||
Message: "Failed to marshal configuration",
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
// Store in Redis with expiry
|
||||
err = s.redisClient.Set(key, string(configBytes), configExpiry)
|
||||
if err != nil {
|
||||
return &ConfigResponse{
|
||||
Success: false,
|
||||
Error: &struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Code: "REDIS_ERROR",
|
||||
Message: "Failed to store configuration in Redis",
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &ConfigResponse{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetConfig retrieves configuration from Redis
|
||||
func (s *RedisConfigStore) GetConfig(serverName string, uid string) (map[string]string, error) {
|
||||
key := GetConfigStoreKey(serverName, uid)
|
||||
|
||||
// Get from Redis
|
||||
value, err := s.redisClient.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(value), &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
181
plugins/golang-filter/mcp-server/handler/rate_limit_handler.go
Normal file
181
plugins/golang-filter/mcp-server/handler/rate_limit_handler.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type MCPRatelimitHandler struct {
|
||||
redisClient *internal.RedisClient
|
||||
callbacks api.FilterCallbackHandler
|
||||
limit int // Maximum requests allowed per window
|
||||
window int // Time window in seconds
|
||||
whitelist []string // Whitelist of UIDs that bypass rate limiting
|
||||
errorText string // Error text to be displayed
|
||||
}
|
||||
|
||||
// MCPRatelimitConfig is the configuration for the rate limit handler
|
||||
type MCPRatelimitConfig struct {
|
||||
Limit int `json:"limit"`
|
||||
Window int `json:"window"`
|
||||
Whitelist []string `json:"white_list"` // List of UIDs that bypass rate limiting
|
||||
ErrorText string `json:"error_text"` // Error text to be displayed
|
||||
}
|
||||
|
||||
// NewMCPRatelimitHandler creates a new rate limit handler
|
||||
func NewMCPRatelimitHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
|
||||
if conf == nil {
|
||||
conf = &MCPRatelimitConfig{
|
||||
Limit: 100,
|
||||
Window: int(24 * time.Hour / time.Second), // 24 hours in seconds
|
||||
Whitelist: []string{},
|
||||
ErrorText: "API rate limit exceeded",
|
||||
}
|
||||
}
|
||||
return &MCPRatelimitHandler{
|
||||
redisClient: redisClient,
|
||||
callbacks: callbacks,
|
||||
limit: conf.Limit,
|
||||
window: conf.Window,
|
||||
whitelist: conf.Whitelist,
|
||||
errorText: conf.ErrorText,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// Lua script for rate limiting
|
||||
LimitScript = `
|
||||
local ttl = redis.call('ttl', KEYS[1])
|
||||
if ttl < 0 then
|
||||
redis.call('set', KEYS[1], ARGV[1] - 1, 'EX', ARGV[2])
|
||||
return {ARGV[1], ARGV[1] - 1, ARGV[2]}
|
||||
end
|
||||
return {ARGV[1], redis.call('incrby', KEYS[1], -1), ttl}
|
||||
`
|
||||
)
|
||||
|
||||
type LimitContext struct {
|
||||
Count int // Current request count
|
||||
Remaining int // Remaining requests allowed
|
||||
Reset int // Time until reset in seconds
|
||||
}
|
||||
|
||||
// TODO: needs to be refactored, rate limit should be registered as a request hook in MCP server
|
||||
func (h *MCPRatelimitHandler) HandleRatelimit(req *http.Request, body []byte) bool {
|
||||
parts := strings.Split(req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
|
||||
// Check if the UID is in whitelist
|
||||
for _, whitelistedUID := range h.whitelist {
|
||||
if whitelistedUID == uid {
|
||||
return true // Bypass rate limiting for whitelisted UIDs
|
||||
}
|
||||
}
|
||||
|
||||
// Build rate limit key using serverName, uid, window and limit
|
||||
limitKey := fmt.Sprintf("mcp-server-limit:%s:%s:%d:%d", serverName, uid, h.window, h.limit)
|
||||
keys := []string{limitKey}
|
||||
|
||||
args := []interface{}{h.limit, h.window}
|
||||
|
||||
result, err := h.redisClient.Eval(LimitScript, 1, keys, args)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to check rate limit: %v", err)
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
|
||||
// Process response
|
||||
resultArray, ok := result.([]interface{})
|
||||
if !ok || len(resultArray) != 3 {
|
||||
api.LogErrorf("Invalid response format: %v", result)
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
|
||||
context := LimitContext{
|
||||
Count: parseRedisValue(resultArray[0]),
|
||||
Remaining: parseRedisValue(resultArray[1]),
|
||||
Reset: parseRedisValue(resultArray[2]),
|
||||
}
|
||||
|
||||
if context.Remaining < 0 {
|
||||
// Create error response content
|
||||
errorContent := []mcp.TextContent{
|
||||
{
|
||||
Type: "text",
|
||||
Text: h.errorText,
|
||||
},
|
||||
}
|
||||
// Create response result
|
||||
result := map[string]interface{}{
|
||||
"content": errorContent,
|
||||
"isError": true,
|
||||
}
|
||||
// Create JSON-RPC response
|
||||
id := getJSONPRCID(body)
|
||||
response := mcp.JSONRPCResponse{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Result: result,
|
||||
}
|
||||
// Convert response to JSON
|
||||
jsonResponse, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to marshal JSON response: %v", err)
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
// Send JSON-RPC response
|
||||
sessionID := req.URL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusAccepted, string(jsonResponse), nil, 0, "")
|
||||
} else {
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, string(jsonResponse), nil, 0, "")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getJSONPRCID(body []byte) mcp.RequestId {
|
||||
baseMessage := struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
}{}
|
||||
if err := json.Unmarshal(body, &baseMessage); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return ""
|
||||
}
|
||||
return baseMessage.ID
|
||||
}
|
||||
|
||||
// parseRedisValue converts the value from Redis to an int
|
||||
func parseRedisValue(value interface{}) int {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case string:
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
76
plugins/golang-filter/mcp-server/internal/crypto.go
Normal file
76
plugins/golang-filter/mcp-server/internal/crypto.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Crypto handles encryption and decryption operations using AES-GCM
|
||||
type Crypto struct {
|
||||
gcm cipher.AEAD
|
||||
}
|
||||
|
||||
func NewCrypto(secret string) (*Crypto, error) {
|
||||
if secret == "" {
|
||||
return nil, fmt.Errorf("secret cannot be empty")
|
||||
}
|
||||
|
||||
// Generate a 32-byte key using SHA-256
|
||||
hash := sha256.Sum256([]byte(secret))
|
||||
block, err := aes.NewCipher(hash[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %v", err)
|
||||
}
|
||||
|
||||
// Create GCM mode
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %v", err)
|
||||
}
|
||||
|
||||
return &Crypto{gcm: gcm}, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts the plaintext data using AES-GCM
|
||||
func (c *Crypto) Encrypt(plaintext []byte) (string, error) {
|
||||
// Generate random nonce
|
||||
nonce := make([]byte, c.gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("failed to generate nonce: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt and authenticate data
|
||||
ciphertext := c.gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts the encrypted string using AES-GCM
|
||||
func (c *Crypto) Decrypt(encryptedStr string) ([]byte, error) {
|
||||
// Decode base64
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encryptedStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid encrypted data format")
|
||||
}
|
||||
|
||||
// Check if the ciphertext is too short
|
||||
if len(ciphertext) < c.gcm.NonceSize() {
|
||||
return nil, fmt.Errorf("invalid encrypted data length")
|
||||
}
|
||||
|
||||
// Extract nonce and ciphertext
|
||||
nonce := ciphertext[:c.gcm.NonceSize()]
|
||||
ciphertext = ciphertext[c.gcm.NonceSize():]
|
||||
|
||||
// Decrypt and verify data
|
||||
plaintext, err := c.gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
89
plugins/golang-filter/mcp-server/internal/match.go
Normal file
89
plugins/golang-filter/mcp-server/internal/match.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RuleType defines the type of matching rule
|
||||
type RuleType string
|
||||
|
||||
const (
|
||||
ExactMatch RuleType = "exact"
|
||||
PrefixMatch RuleType = "prefix"
|
||||
SuffixMatch RuleType = "suffix"
|
||||
ContainsMatch RuleType = "contains"
|
||||
RegexMatch RuleType = "regex"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// convertWildcardToRegex converts wildcard pattern to regex pattern
|
||||
func convertWildcardToRegex(pattern string) string {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
|
||||
return pattern
|
||||
}
|
||||
|
||||
// matchPattern checks if the target matches the pattern based on rule type
|
||||
func matchPattern(pattern string, target string, ruleType RuleType) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
switch ruleType {
|
||||
case ExactMatch:
|
||||
return pattern == target
|
||||
case PrefixMatch:
|
||||
return strings.HasPrefix(target, pattern)
|
||||
case SuffixMatch:
|
||||
return strings.HasSuffix(target, pattern)
|
||||
case ContainsMatch:
|
||||
return strings.Contains(target, pattern)
|
||||
case RegexMatch:
|
||||
matched, err := regexp.MatchString(pattern, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// matchDomain checks if the domain matches the pattern
|
||||
func matchDomain(domain string, pattern string) bool {
|
||||
if pattern == "" || pattern == "*" {
|
||||
return true
|
||||
}
|
||||
// Convert wildcard pattern to regex pattern
|
||||
regexPattern := convertWildcardToRegex(pattern)
|
||||
matched, _ := regexp.MatchString(regexPattern, domain)
|
||||
return matched
|
||||
}
|
||||
|
||||
// matchDomainAndPath checks if both domain and path match the rule
|
||||
func matchDomainAndPath(domain, path string, rule MatchRule) bool {
|
||||
return matchDomain(domain, rule.MatchRuleDomain) &&
|
||||
matchPattern(rule.MatchRulePath, path, rule.MatchRuleType)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if len(rules) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if matchDomainAndPath(host, path, rule) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
266
plugins/golang-filter/mcp-server/internal/redis.go
Normal file
266
plugins/golang-filter/mcp-server/internal/redis.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
type RedisConfig struct {
|
||||
address string
|
||||
username string
|
||||
password string
|
||||
db int
|
||||
secret string // Encryption key
|
||||
}
|
||||
|
||||
// ParseRedisConfig parses Redis configuration from a map
|
||||
func ParseRedisConfig(config map[string]interface{}) (*RedisConfig, error) {
|
||||
c := &RedisConfig{}
|
||||
|
||||
// address is required
|
||||
if addr, ok := config["address"].(string); ok && addr != "" {
|
||||
c.address = addr
|
||||
} else {
|
||||
return nil, fmt.Errorf("address is required and must be a non-empty string")
|
||||
}
|
||||
|
||||
// username is optional
|
||||
if username, ok := config["username"].(string); ok {
|
||||
c.username = username
|
||||
}
|
||||
|
||||
// password is optional
|
||||
if password, ok := config["password"].(string); ok {
|
||||
c.password = password
|
||||
}
|
||||
|
||||
// db is optional, default to 0
|
||||
if db, ok := config["db"].(int); ok {
|
||||
c.db = db
|
||||
}
|
||||
|
||||
// secret is optional
|
||||
if secret, ok := config["secret"].(string); ok {
|
||||
c.secret = secret
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// RedisClient is a struct to handle Redis connections and operations
|
||||
type RedisClient struct {
|
||||
client *redis.Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
config *RedisConfig
|
||||
crypto *Crypto
|
||||
}
|
||||
|
||||
// NewRedisClient creates a new RedisClient instance and establishes a connection to the Redis server
|
||||
func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: config.address,
|
||||
Username: config.username,
|
||||
Password: config.password,
|
||||
DB: config.db,
|
||||
})
|
||||
|
||||
// Ping the Redis server to check the connection
|
||||
pong, err := client.Ping(context.Background()).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
}
|
||||
api.LogDebugf("Connected to Redis: %s", pong)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var crypto *Crypto
|
||||
if config.secret != "" {
|
||||
crypto, err = NewCrypto(config.secret)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
redisClient := &RedisClient{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
config: config,
|
||||
crypto: crypto,
|
||||
}
|
||||
|
||||
// Start keep-alive check
|
||||
go redisClient.keepAlive()
|
||||
|
||||
return redisClient, nil
|
||||
}
|
||||
|
||||
// keepAlive periodically checks Redis connection and attempts to reconnect if needed
|
||||
func (r *RedisClient) keepAlive() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := r.checkConnection(); err != nil {
|
||||
api.LogErrorf("Redis connection check failed: %v", err)
|
||||
if err := r.reconnect(); err != nil {
|
||||
api.LogErrorf("Failed to reconnect to Redis: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkConnection verifies if the Redis connection is still alive
|
||||
func (r *RedisClient) checkConnection() error {
|
||||
_, err := r.client.Ping(r.ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
// reconnect attempts to establish a new connection to Redis
|
||||
func (r *RedisClient) reconnect() error {
|
||||
// Close the old client
|
||||
if err := r.client.Close(); err != nil {
|
||||
api.LogErrorf("Error closing old Redis connection: %v", err)
|
||||
}
|
||||
|
||||
// Create new client
|
||||
r.client = redis.NewClient(&redis.Options{
|
||||
Addr: r.config.address,
|
||||
Username: r.config.username,
|
||||
Password: r.config.password,
|
||||
DB: r.config.db,
|
||||
})
|
||||
|
||||
// Test the new connection
|
||||
if err := r.checkConnection(); err != nil {
|
||||
return fmt.Errorf("failed to reconnect to Redis: %w", err)
|
||||
}
|
||||
|
||||
api.LogDebugf("Successfully reconnected to Redis")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Publish publishes a message to a Redis channel
|
||||
func (r *RedisClient) Publish(channel string, message string) error {
|
||||
err := r.client.Publish(r.ctx, channel, message).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to publish message: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a Redis channel and processes messages
|
||||
func (r *RedisClient) Subscribe(channel string, stopChan chan struct{}, callback func(message string)) error {
|
||||
pubsub := r.client.Subscribe(r.ctx, channel)
|
||||
_, err := pubsub.Receive(r.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to subscribe to channel: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Redis Subscribe recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
pubsub.Close()
|
||||
api.LogDebugf("Closed subscription to channel %s", channel)
|
||||
}()
|
||||
|
||||
ch := pubsub.Channel()
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
api.LogDebugf("Stopping subscription to channel %s", channel)
|
||||
return
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
api.LogDebugf("Redis subscription channel closed for %s", channel)
|
||||
return
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Recovered from panic in callback: %v", r)
|
||||
}
|
||||
}()
|
||||
callback(msg.Payload)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set sets the value of a key in Redis
|
||||
func (r *RedisClient) Set(key string, value string, expiration time.Duration) error {
|
||||
var finalValue string
|
||||
if r.crypto != nil {
|
||||
// Encrypt the data
|
||||
encryptedValue, err := r.crypto.Encrypt([]byte(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt value: %w", err)
|
||||
}
|
||||
finalValue = encryptedValue
|
||||
} else {
|
||||
finalValue = value
|
||||
}
|
||||
|
||||
err := r.client.Set(r.ctx, key, finalValue, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set key: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves the value of a key from Redis
|
||||
func (r *RedisClient) Get(key string) (string, error) {
|
||||
value, err := r.client.Get(r.ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("key does not exist")
|
||||
} else if err != nil {
|
||||
return "", fmt.Errorf("failed to get key: %w", err)
|
||||
}
|
||||
|
||||
if r.crypto != nil {
|
||||
// Decrypt the data
|
||||
decryptedValue, err := r.crypto.Decrypt(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt value: %w", err)
|
||||
}
|
||||
return string(decryptedValue), nil
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Close closes the Redis client and stops the keepalive goroutine
|
||||
func (r *RedisClient) Close() error {
|
||||
r.cancel()
|
||||
return r.client.Close()
|
||||
}
|
||||
|
||||
// Eval executes a Lua script
|
||||
func (r *RedisClient) Eval(script string, numKeys int, keys []string, args []interface{}) (interface{}, error) {
|
||||
result, err := r.client.Eval(r.ctx, script, keys, args...).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute Lua script: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
26
plugins/golang-filter/mcp-server/internal/registry.go
Normal file
26
plugins/golang-filter/mcp-server/internal/registry.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package internal
|
||||
|
||||
var GlobalRegistry = NewServerRegistry()
|
||||
|
||||
type Server interface {
|
||||
ParseConfig(config map[string]any) error
|
||||
NewServer(serverName string) (*MCPServer, error)
|
||||
}
|
||||
|
||||
type ServerRegistry struct {
|
||||
servers map[string]Server
|
||||
}
|
||||
|
||||
func NewServerRegistry() *ServerRegistry {
|
||||
return &ServerRegistry{
|
||||
servers: make(map[string]Server),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ServerRegistry) RegisterServer(name string, server Server) {
|
||||
r.servers[name] = server
|
||||
}
|
||||
|
||||
func (r *ServerRegistry) GetServer(name string) Server {
|
||||
return r.servers[name]
|
||||
}
|
||||
864
plugins/golang-filter/mcp-server/internal/server.go
Normal file
864
plugins/golang-filter/mcp-server/internal/server.go
Normal file
@@ -0,0 +1,864 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// resourceEntry holds both a resource and its handler
|
||||
type resourceEntry struct {
|
||||
resource mcp.Resource
|
||||
handler ResourceHandlerFunc
|
||||
}
|
||||
|
||||
// resourceTemplateEntry holds both a template and its handler
|
||||
type resourceTemplateEntry struct {
|
||||
template mcp.ResourceTemplate
|
||||
handler ResourceTemplateHandlerFunc
|
||||
}
|
||||
|
||||
// ServerOption is a function that configures an MCPServer.
|
||||
type ServerOption func(*MCPServer)
|
||||
|
||||
// ResourceHandlerFunc is a function that returns resource contents.
|
||||
type ResourceHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
|
||||
|
||||
// ResourceTemplateHandlerFunc is a function that returns a resource template.
|
||||
type ResourceTemplateHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
|
||||
|
||||
// PromptHandlerFunc handles prompt requests with given arguments.
|
||||
type PromptHandlerFunc func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error)
|
||||
|
||||
// ToolHandlerFunc handles tool calls with given arguments.
|
||||
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
|
||||
// ServerTool combines a Tool with its ToolHandlerFunc.
|
||||
type ServerTool struct {
|
||||
Tool mcp.Tool
|
||||
Handler ToolHandlerFunc
|
||||
}
|
||||
|
||||
// NotificationContext provides client identification for notifications
|
||||
type NotificationContext struct {
|
||||
ClientID string
|
||||
SessionID string
|
||||
}
|
||||
|
||||
// ServerNotification combines the notification with client context
|
||||
type ServerNotification struct {
|
||||
Context NotificationContext
|
||||
Notification mcp.JSONRPCNotification
|
||||
}
|
||||
|
||||
// NotificationHandlerFunc handles incoming notifications.
|
||||
type NotificationHandlerFunc func(ctx context.Context, notification mcp.JSONRPCNotification)
|
||||
|
||||
// MCPServer implements a Model Control Protocol server that can handle various types of requests
|
||||
// including resources, prompts, and tools.
|
||||
type MCPServer struct {
|
||||
mu sync.RWMutex // Add mutex for protecting shared resources
|
||||
name string
|
||||
version string
|
||||
instructions string
|
||||
resources map[string]resourceEntry
|
||||
resourceTemplates map[string]resourceTemplateEntry
|
||||
prompts map[string]mcp.Prompt
|
||||
promptHandlers map[string]PromptHandlerFunc
|
||||
tools map[string]ServerTool
|
||||
notificationHandlers map[string]NotificationHandlerFunc
|
||||
capabilities serverCapabilities
|
||||
notifications chan ServerNotification
|
||||
clientMu sync.Mutex // Separate mutex for client context
|
||||
currentClient NotificationContext
|
||||
initialized atomic.Bool // Use atomic for the initialized flag
|
||||
destory chan struct{}
|
||||
}
|
||||
|
||||
// serverKey is the context key for storing the server instance
|
||||
type serverKey struct{}
|
||||
|
||||
// ServerFromContext retrieves the MCPServer instance from a context
|
||||
func ServerFromContext(ctx context.Context) *MCPServer {
|
||||
if srv, ok := ctx.Value(serverKey{}).(*MCPServer); ok {
|
||||
return srv
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithContext sets the current client context and returns the provided context
|
||||
func (s *MCPServer) WithContext(
|
||||
ctx context.Context,
|
||||
notifCtx NotificationContext,
|
||||
) context.Context {
|
||||
s.clientMu.Lock()
|
||||
s.currentClient = notifCtx
|
||||
s.clientMu.Unlock()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// SendNotificationToClient sends a notification to the current client
|
||||
func (s *MCPServer) SendNotificationToClient(
|
||||
method string,
|
||||
params map[string]interface{},
|
||||
) error {
|
||||
if s.notifications == nil {
|
||||
return fmt.Errorf("notification channel not initialized")
|
||||
}
|
||||
|
||||
s.clientMu.Lock()
|
||||
clientContext := s.currentClient
|
||||
s.clientMu.Unlock()
|
||||
|
||||
notification := mcp.JSONRPCNotification{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
Notification: mcp.Notification{
|
||||
Method: method,
|
||||
Params: mcp.NotificationParams{
|
||||
AdditionalFields: params,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
select {
|
||||
case s.notifications <- ServerNotification{
|
||||
Context: clientContext,
|
||||
Notification: notification,
|
||||
}:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("notification channel full or blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// serverCapabilities defines the supported features of the MCP server
|
||||
type serverCapabilities struct {
|
||||
tools *toolCapabilities
|
||||
resources *resourceCapabilities
|
||||
prompts *promptCapabilities
|
||||
logging bool
|
||||
}
|
||||
|
||||
// resourceCapabilities defines the supported resource-related features
|
||||
type resourceCapabilities struct {
|
||||
subscribe bool
|
||||
listChanged bool
|
||||
}
|
||||
|
||||
// promptCapabilities defines the supported prompt-related features
|
||||
type promptCapabilities struct {
|
||||
listChanged bool
|
||||
}
|
||||
|
||||
// toolCapabilities defines the supported tool-related features
|
||||
type toolCapabilities struct {
|
||||
listChanged bool
|
||||
}
|
||||
|
||||
// WithResourceCapabilities configures resource-related server capabilities
|
||||
func WithResourceCapabilities(subscribe, listChanged bool) ServerOption {
|
||||
return func(s *MCPServer) {
|
||||
// Always create a non-nil capability object
|
||||
s.capabilities.resources = &resourceCapabilities{
|
||||
subscribe: subscribe,
|
||||
listChanged: listChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithPromptCapabilities configures prompt-related server capabilities
|
||||
func WithPromptCapabilities(listChanged bool) ServerOption {
|
||||
return func(s *MCPServer) {
|
||||
// Always create a non-nil capability object
|
||||
s.capabilities.prompts = &promptCapabilities{
|
||||
listChanged: listChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithToolCapabilities configures tool-related server capabilities
|
||||
func WithToolCapabilities(listChanged bool) ServerOption {
|
||||
return func(s *MCPServer) {
|
||||
// Always create a non-nil capability object
|
||||
s.capabilities.tools = &toolCapabilities{
|
||||
listChanged: listChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogging enables logging capabilities for the server
|
||||
func WithLogging() ServerOption {
|
||||
return func(s *MCPServer) {
|
||||
s.capabilities.logging = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithInstructions sets the server instructions for the client returned in the initialize response
|
||||
func WithInstructions(instructions string) ServerOption {
|
||||
return func(s *MCPServer) {
|
||||
s.instructions = instructions
|
||||
}
|
||||
}
|
||||
|
||||
// NewMCPServer creates a new MCP server instance with the given name, version and options
|
||||
func NewMCPServer(
|
||||
name, version string,
|
||||
opts ...ServerOption,
|
||||
) *MCPServer {
|
||||
s := &MCPServer{
|
||||
resources: make(map[string]resourceEntry),
|
||||
resourceTemplates: make(map[string]resourceTemplateEntry),
|
||||
prompts: make(map[string]mcp.Prompt),
|
||||
promptHandlers: make(map[string]PromptHandlerFunc),
|
||||
tools: make(map[string]ServerTool),
|
||||
name: name,
|
||||
version: version,
|
||||
notificationHandlers: make(map[string]NotificationHandlerFunc),
|
||||
notifications: make(chan ServerNotification, 100),
|
||||
capabilities: serverCapabilities{
|
||||
tools: nil,
|
||||
resources: nil,
|
||||
prompts: nil,
|
||||
logging: false,
|
||||
},
|
||||
destory: make(chan struct{}),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// HandleMessage processes an incoming JSON-RPC message and returns an appropriate response
|
||||
func (s *MCPServer) HandleMessage(
|
||||
ctx context.Context,
|
||||
message json.RawMessage,
|
||||
) mcp.JSONRPCMessage {
|
||||
// Add server to context
|
||||
ctx = context.WithValue(ctx, serverKey{}, s)
|
||||
|
||||
var baseMessage struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &baseMessage); err != nil {
|
||||
return createErrorResponse(
|
||||
nil,
|
||||
mcp.PARSE_ERROR,
|
||||
"Failed to parse message",
|
||||
)
|
||||
}
|
||||
|
||||
// Check for valid JSONRPC version
|
||||
if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid JSON-RPC version",
|
||||
)
|
||||
}
|
||||
|
||||
if baseMessage.ID == nil {
|
||||
var notification mcp.JSONRPCNotification
|
||||
if err := json.Unmarshal(message, ¬ification); err != nil {
|
||||
return createErrorResponse(
|
||||
nil,
|
||||
mcp.PARSE_ERROR,
|
||||
"Failed to parse notification",
|
||||
)
|
||||
}
|
||||
s.handleNotification(ctx, notification)
|
||||
return nil // Return nil for notifications
|
||||
}
|
||||
|
||||
switch baseMessage.Method {
|
||||
case "initialize":
|
||||
var request mcp.InitializeRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid initialize request",
|
||||
)
|
||||
}
|
||||
return s.handleInitialize(ctx, baseMessage.ID, request)
|
||||
case "ping":
|
||||
var request mcp.PingRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid ping request",
|
||||
)
|
||||
}
|
||||
return s.handlePing(ctx, baseMessage.ID, request)
|
||||
case "resources/list":
|
||||
if s.capabilities.resources == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Resources not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.ListResourcesRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid list resources request",
|
||||
)
|
||||
}
|
||||
return s.handleListResources(ctx, baseMessage.ID, request)
|
||||
case "resources/templates/list":
|
||||
if s.capabilities.resources == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Resources not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.ListResourceTemplatesRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid list resource templates request",
|
||||
)
|
||||
}
|
||||
return s.handleListResourceTemplates(ctx, baseMessage.ID, request)
|
||||
case "resources/read":
|
||||
if s.capabilities.resources == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Resources not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.ReadResourceRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid read resource request",
|
||||
)
|
||||
}
|
||||
return s.handleReadResource(ctx, baseMessage.ID, request)
|
||||
case "prompts/list":
|
||||
if s.capabilities.prompts == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Prompts not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.ListPromptsRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid list prompts request",
|
||||
)
|
||||
}
|
||||
return s.handleListPrompts(ctx, baseMessage.ID, request)
|
||||
case "prompts/get":
|
||||
if s.capabilities.prompts == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Prompts not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.GetPromptRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid get prompt request",
|
||||
)
|
||||
}
|
||||
return s.handleGetPrompt(ctx, baseMessage.ID, request)
|
||||
case "tools/list":
|
||||
if s.capabilities.tools == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Tools not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.ListToolsRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid list tools request",
|
||||
)
|
||||
}
|
||||
return s.handleListTools(ctx, baseMessage.ID, request)
|
||||
case "tools/call":
|
||||
if s.capabilities.tools == nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
"Tools not supported",
|
||||
)
|
||||
}
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid call tool request",
|
||||
)
|
||||
}
|
||||
return s.handleToolCall(ctx, baseMessage.ID, request)
|
||||
case "":
|
||||
var response mcp.JSONRPCResponse
|
||||
if err := json.Unmarshal(message, &response); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid message format",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.METHOD_NOT_FOUND,
|
||||
fmt.Sprintf("Method %s not found", baseMessage.Method),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// AddResource registers a new resource and its handler
|
||||
func (s *MCPServer) AddResource(
|
||||
resource mcp.Resource,
|
||||
handler ResourceHandlerFunc,
|
||||
) {
|
||||
if s.capabilities.resources == nil {
|
||||
s.capabilities.resources = &resourceCapabilities{}
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.resources[resource.URI] = resourceEntry{
|
||||
resource: resource,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
// AddResourceTemplate registers a new resource template and its handler
|
||||
func (s *MCPServer) AddResourceTemplate(
|
||||
template mcp.ResourceTemplate,
|
||||
handler ResourceTemplateHandlerFunc,
|
||||
) {
|
||||
if s.capabilities.resources == nil {
|
||||
s.capabilities.resources = &resourceCapabilities{}
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.resourceTemplates[template.URITemplate] = resourceTemplateEntry{
|
||||
template: template,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
// AddPrompt registers a new prompt handler with the given name
|
||||
func (s *MCPServer) AddPrompt(prompt mcp.Prompt, handler PromptHandlerFunc) {
|
||||
if s.capabilities.prompts == nil {
|
||||
s.capabilities.prompts = &promptCapabilities{}
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.prompts[prompt.Name] = prompt
|
||||
s.promptHandlers[prompt.Name] = handler
|
||||
}
|
||||
|
||||
// AddTool registers a new tool and its handler
|
||||
func (s *MCPServer) AddTool(tool mcp.Tool, handler ToolHandlerFunc) {
|
||||
s.AddTools(ServerTool{Tool: tool, Handler: handler})
|
||||
}
|
||||
|
||||
// AddTools registers multiple tools at once
|
||||
func (s *MCPServer) AddTools(tools ...ServerTool) {
|
||||
if s.capabilities.tools == nil {
|
||||
s.capabilities.tools = &toolCapabilities{}
|
||||
}
|
||||
s.mu.Lock()
|
||||
for _, entry := range tools {
|
||||
s.tools[entry.Tool.Name] = entry
|
||||
}
|
||||
initialized := s.initialized.Load()
|
||||
s.mu.Unlock()
|
||||
|
||||
// Send notification if server is already initialized
|
||||
if initialized {
|
||||
if err := s.SendNotificationToClient("notifications/tools/list_changed", nil); err != nil {
|
||||
// We can't return the error, but in a future version we could log it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetTools replaces all existing tools with the provided list
|
||||
func (s *MCPServer) SetTools(tools ...ServerTool) {
|
||||
s.mu.Lock()
|
||||
s.tools = make(map[string]ServerTool)
|
||||
s.mu.Unlock()
|
||||
s.AddTools(tools...)
|
||||
}
|
||||
|
||||
// DeleteTools removes a tool from the server
|
||||
func (s *MCPServer) DeleteTools(names ...string) {
|
||||
s.mu.Lock()
|
||||
for _, name := range names {
|
||||
delete(s.tools, name)
|
||||
}
|
||||
initialized := s.initialized.Load()
|
||||
s.mu.Unlock()
|
||||
|
||||
// Send notification if server is already initialized
|
||||
if initialized {
|
||||
if err := s.SendNotificationToClient("notifications/tools/list_changed", nil); err != nil {
|
||||
// We can't return the error, but in a future version we could log it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddNotificationHandler registers a new handler for incoming notifications
|
||||
func (s *MCPServer) AddNotificationHandler(
|
||||
method string,
|
||||
handler NotificationHandlerFunc,
|
||||
) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.notificationHandlers[method] = handler
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleInitialize(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.InitializeRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
capabilities := mcp.ServerCapabilities{}
|
||||
|
||||
// Only add resource capabilities if they're configured
|
||||
if s.capabilities.resources != nil {
|
||||
capabilities.Resources = &struct {
|
||||
Subscribe bool `json:"subscribe,omitempty"`
|
||||
ListChanged bool `json:"listChanged,omitempty"`
|
||||
}{
|
||||
Subscribe: s.capabilities.resources.subscribe,
|
||||
ListChanged: s.capabilities.resources.listChanged,
|
||||
}
|
||||
}
|
||||
|
||||
// Only add prompt capabilities if they're configured
|
||||
if s.capabilities.prompts != nil {
|
||||
capabilities.Prompts = &struct {
|
||||
ListChanged bool `json:"listChanged,omitempty"`
|
||||
}{
|
||||
ListChanged: s.capabilities.prompts.listChanged,
|
||||
}
|
||||
}
|
||||
|
||||
// Only add tool capabilities if they're configured
|
||||
if s.capabilities.tools != nil {
|
||||
capabilities.Tools = &struct {
|
||||
ListChanged bool `json:"listChanged,omitempty"`
|
||||
}{
|
||||
ListChanged: s.capabilities.tools.listChanged,
|
||||
}
|
||||
}
|
||||
|
||||
if s.capabilities.logging {
|
||||
capabilities.Logging = &struct{}{}
|
||||
}
|
||||
|
||||
result := mcp.InitializeResult{
|
||||
ProtocolVersion: request.Params.ProtocolVersion,
|
||||
ServerInfo: mcp.Implementation{
|
||||
Name: s.name,
|
||||
Version: s.version,
|
||||
},
|
||||
Capabilities: capabilities,
|
||||
Instructions: s.instructions,
|
||||
}
|
||||
|
||||
s.initialized.Store(true)
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handlePing(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.PingRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
return createResponse(id, mcp.EmptyResult{})
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleListResources(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.ListResourcesRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
resources := make([]mcp.Resource, 0, len(s.resources))
|
||||
for _, entry := range s.resources {
|
||||
resources = append(resources, entry.resource)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
result := mcp.ListResourcesResult{
|
||||
Resources: resources,
|
||||
}
|
||||
if request.Params.Cursor != "" {
|
||||
result.NextCursor = "" // Handle pagination if needed
|
||||
}
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleListResourceTemplates(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.ListResourceTemplatesRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
templates := make([]mcp.ResourceTemplate, 0, len(s.resourceTemplates))
|
||||
for _, entry := range s.resourceTemplates {
|
||||
templates = append(templates, entry.template)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
result := mcp.ListResourceTemplatesResult{
|
||||
ResourceTemplates: templates,
|
||||
}
|
||||
if request.Params.Cursor != "" {
|
||||
result.NextCursor = "" // Handle pagination if needed
|
||||
}
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleReadResource(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.ReadResourceRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
// First try direct resource handlers
|
||||
if entry, ok := s.resources[request.Params.URI]; ok {
|
||||
handler := entry.handler
|
||||
s.mu.RUnlock()
|
||||
contents, err := handler(ctx, request)
|
||||
if err != nil {
|
||||
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
|
||||
}
|
||||
return createResponse(id, mcp.ReadResourceResult{Contents: contents})
|
||||
}
|
||||
|
||||
// If no direct handler found, try matching against templates
|
||||
var matchedHandler ResourceTemplateHandlerFunc
|
||||
var matched bool
|
||||
for uriTemplate, entry := range s.resourceTemplates {
|
||||
if matchesTemplate(request.Params.URI, uriTemplate) {
|
||||
matchedHandler = entry.handler
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
if matched {
|
||||
contents, err := matchedHandler(ctx, request)
|
||||
if err != nil {
|
||||
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
|
||||
}
|
||||
return createResponse(
|
||||
id,
|
||||
mcp.ReadResourceResult{Contents: contents},
|
||||
)
|
||||
}
|
||||
|
||||
return createErrorResponse(
|
||||
id,
|
||||
mcp.INVALID_PARAMS,
|
||||
fmt.Sprintf(
|
||||
"No handler found for resource URI: %s",
|
||||
request.Params.URI,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// matchesTemplate checks if a URI matches a URI template pattern
|
||||
func matchesTemplate(uri string, template string) bool {
|
||||
// Convert template into a regex pattern
|
||||
pattern := template
|
||||
// Replace {name} with ([^/]+)
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = regexp.MustCompile(`\\\{[^}]+\\\}`).
|
||||
ReplaceAllString(pattern, `([^/]+)`)
|
||||
pattern = "^" + pattern + "$"
|
||||
|
||||
matched, _ := regexp.MatchString(pattern, uri)
|
||||
return matched
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleListPrompts(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.ListPromptsRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
prompts := make([]mcp.Prompt, 0, len(s.prompts))
|
||||
for _, prompt := range s.prompts {
|
||||
prompts = append(prompts, prompt)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
result := mcp.ListPromptsResult{
|
||||
Prompts: prompts,
|
||||
}
|
||||
if request.Params.Cursor != "" {
|
||||
result.NextCursor = "" // Handle pagination if needed
|
||||
}
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleGetPrompt(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.GetPromptRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
handler, ok := s.promptHandlers[request.Params.Name]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return createErrorResponse(
|
||||
id,
|
||||
mcp.INVALID_PARAMS,
|
||||
fmt.Sprintf("Prompt not found: %s", request.Params.Name),
|
||||
)
|
||||
}
|
||||
|
||||
result, err := handler(ctx, request)
|
||||
if err != nil {
|
||||
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
|
||||
}
|
||||
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleListTools(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.ListToolsRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
tools := make([]mcp.Tool, 0, len(s.tools))
|
||||
|
||||
// Get all tool names for consistent ordering
|
||||
toolNames := make([]string, 0, len(s.tools))
|
||||
for name := range s.tools {
|
||||
toolNames = append(toolNames, name)
|
||||
}
|
||||
|
||||
// Sort the tool names for consistent ordering
|
||||
sort.Strings(toolNames)
|
||||
|
||||
// Add tools in sorted order
|
||||
for _, name := range toolNames {
|
||||
tools = append(tools, s.tools[name].Tool)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
result := mcp.ListToolsResult{
|
||||
Tools: tools,
|
||||
}
|
||||
if request.Params.Cursor != "" {
|
||||
result.NextCursor = "" // Handle pagination if needed
|
||||
}
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleToolCall(
|
||||
ctx context.Context,
|
||||
id interface{},
|
||||
request mcp.CallToolRequest,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
tool, ok := s.tools[request.Params.Name]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return createErrorResponse(
|
||||
id,
|
||||
mcp.INVALID_PARAMS,
|
||||
fmt.Sprintf("Tool not found: %s", request.Params.Name),
|
||||
)
|
||||
}
|
||||
|
||||
result, err := tool.Handler(ctx, request)
|
||||
if err != nil {
|
||||
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
|
||||
}
|
||||
|
||||
return createResponse(id, result)
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleNotification(
|
||||
ctx context.Context,
|
||||
notification mcp.JSONRPCNotification,
|
||||
) mcp.JSONRPCMessage {
|
||||
s.mu.RLock()
|
||||
handler, ok := s.notificationHandlers[notification.Method]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if ok {
|
||||
handler(ctx, notification)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MCPServer) Close() {
|
||||
close(s.destory)
|
||||
}
|
||||
|
||||
func (s *MCPServer) GetDestoryChannel() chan struct{} {
|
||||
return s.destory
|
||||
}
|
||||
|
||||
func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage {
|
||||
return mcp.JSONRPCResponse{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
func createErrorResponse(
|
||||
id interface{},
|
||||
code int,
|
||||
message string,
|
||||
) mcp.JSONRPCMessage {
|
||||
return mcp.JSONRPCError{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Error: struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
255
plugins/golang-filter/mcp-server/internal/sse.go
Normal file
255
plugins/golang-filter/mcp-server/internal/sse.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// GetSSEChannelName returns the Redis channel name for the given session ID
|
||||
func GetSSEChannelName(sessionID string) string {
|
||||
return fmt.Sprintf("mcp-server-sse:%s", sessionID)
|
||||
}
|
||||
|
||||
// SSEServer implements a Server-Sent Events (SSE) based MCP server.
|
||||
// It provides real-time communication capabilities over HTTP using the SSE protocol.
|
||||
type SSEServer struct {
|
||||
server *MCPServer
|
||||
baseURL string
|
||||
messageEndpoint string
|
||||
sseEndpoint string
|
||||
sessions sync.Map
|
||||
redisClient *RedisClient // Redis client for pub/sub
|
||||
}
|
||||
|
||||
func (s *SSEServer) GetMessageEndpoint() string {
|
||||
return s.messageEndpoint
|
||||
}
|
||||
|
||||
func (s *SSEServer) GetSSEEndpoint() string {
|
||||
return s.sseEndpoint
|
||||
}
|
||||
|
||||
func (s *SSEServer) GetServerName() string {
|
||||
return s.server.name
|
||||
}
|
||||
|
||||
// Option defines a function type for configuring SSEServer
|
||||
type Option func(*SSEServer)
|
||||
|
||||
// WithBaseURL sets the base URL for the SSE server
|
||||
func WithBaseURL(baseURL string) Option {
|
||||
return func(s *SSEServer) {
|
||||
s.baseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessageEndpoint sets the message endpoint path
|
||||
func WithMessageEndpoint(endpoint string) Option {
|
||||
return func(s *SSEServer) {
|
||||
s.messageEndpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
// WithSSEEndpoint sets the SSE endpoint path
|
||||
func WithSSEEndpoint(endpoint string) Option {
|
||||
return func(s *SSEServer) {
|
||||
s.sseEndpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
func WithRedisClient(redisClient *RedisClient) Option {
|
||||
return func(s *SSEServer) {
|
||||
s.redisClient = redisClient
|
||||
}
|
||||
}
|
||||
|
||||
// NewSSEServer creates a new SSE server instance with the given MCP server and options.
|
||||
func NewSSEServer(server *MCPServer, opts ...Option) *SSEServer {
|
||||
s := &SSEServer{
|
||||
server: server,
|
||||
sseEndpoint: "/sse",
|
||||
messageEndpoint: "/message",
|
||||
}
|
||||
|
||||
// Apply all options
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// handleSSE handles incoming SSE connection requests.
|
||||
// It sets up appropriate headers and creates a new session for the client.
|
||||
func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct{}) {
|
||||
sessionID := uuid.New().String()
|
||||
|
||||
s.sessions.Store(sessionID, true)
|
||||
defer s.sessions.Delete(sessionID)
|
||||
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
|
||||
messageEndpoint := fmt.Sprintf(
|
||||
"%s%s?sessionId=%s",
|
||||
s.baseURL,
|
||||
s.messageEndpoint,
|
||||
sessionID,
|
||||
)
|
||||
|
||||
// go func() {
|
||||
// for {
|
||||
// select {
|
||||
// case serverNotification := <-s.server.notifications:
|
||||
// // Only forward notifications meant for this session
|
||||
// if serverNotification.Context.SessionID == sessionID {
|
||||
// eventData, err := json.Marshal(serverNotification.Notification)
|
||||
// if err == nil {
|
||||
// select {
|
||||
// case session.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData):
|
||||
// // Event queued successfully
|
||||
// case <-session.done:
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// case <-session.done:
|
||||
// return
|
||||
// case <-r.Context().Done():
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }()
|
||||
|
||||
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))
|
||||
})
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to subscribe to Redis channel: %v", err)
|
||||
}
|
||||
|
||||
// Send the initial endpoint event
|
||||
initialEvent := fmt.Sprintf("event: endpoint\ndata: %s\r\n\r\n", messageEndpoint)
|
||||
err = s.redisClient.Publish(channel, initialEvent)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to send initial event: %v", err)
|
||||
}
|
||||
|
||||
// Start health check handler
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Health check handler recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Send health check message
|
||||
currentTime := time.Now().Format(time.RFC3339)
|
||||
pingRequest := mcp.JSONRPCRequest{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: currentTime,
|
||||
Request: mcp.Request{
|
||||
Method: "ping",
|
||||
},
|
||||
}
|
||||
pingData, _ := json.Marshal(pingRequest)
|
||||
healthCheckEvent := fmt.Sprintf("event: message\ndata: %s\n\n", pingData)
|
||||
if err := s.redisClient.Publish(channel, healthCheckEvent); err != nil {
|
||||
api.LogErrorf("Failed to send health check: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
|
||||
// back through both the SSE connection and HTTP response.
|
||||
func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body json.RawMessage) int {
|
||||
if r.Method != http.MethodPost {
|
||||
s.writeJSONRPCError(w, nil, mcp.INVALID_REQUEST, fmt.Sprintf("Method %s not allowed", r.Method))
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
sessionID := r.URL.Query().Get("sessionId")
|
||||
// support streamable http without sessionId
|
||||
// if sessionID == "" {
|
||||
// s.writeJSONRPCError(w, nil, mcp.INVALID_PARAMS, "Missing sessionId")
|
||||
// return
|
||||
// }
|
||||
|
||||
// Set the client context in the server before handling the message
|
||||
ctx := s.server.WithContext(r.Context(), NotificationContext{
|
||||
ClientID: sessionID,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
|
||||
//TODO: check session id
|
||||
// _, ok := s.sessions.Load(sessionID)
|
||||
// if !ok {
|
||||
// s.writeJSONRPCError(w, nil, mcp.INVALID_PARAMS, "Invalid session ID")
|
||||
// return
|
||||
// }
|
||||
|
||||
// Process message through MCPServer
|
||||
response := s.server.HandleMessage(ctx, body)
|
||||
var status int
|
||||
// Only send response if there is one (not for notifications)
|
||||
if response != nil {
|
||||
eventData, _ := json.Marshal(response)
|
||||
|
||||
if sessionID != "" && s.redisClient != nil {
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
publishErr := s.redisClient.Publish(channel, fmt.Sprintf("event: message\ndata: %s\n\n", eventData))
|
||||
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish message to Redis: %v", publishErr)
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
// support streamable http
|
||||
w.WriteHeader(http.StatusOK)
|
||||
status = http.StatusOK
|
||||
}
|
||||
// Send HTTP response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
} else {
|
||||
// For notifications, just send 202 Accepted with no body
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// writeJSONRPCError writes a JSON-RPC error response with the given error details.
|
||||
func (s *SSEServer) writeJSONRPCError(
|
||||
w http.ResponseWriter,
|
||||
id interface{},
|
||||
code int,
|
||||
message string,
|
||||
) {
|
||||
response := createErrorResponse(id, code, message)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (s *SSEServer) Close() {
|
||||
s.server.Close()
|
||||
}
|
||||
315
plugins/golang-filter/mcp-server/registry/nacos/nacos.go
Normal file
315
plugins/golang-filter/mcp-server/registry/nacos/nacos.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package nacos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/model"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/vo"
|
||||
)
|
||||
|
||||
type NacosMcpRegsitry struct {
|
||||
serviceMatcher map[string]string
|
||||
configClient config_client.IConfigClient
|
||||
namingClient naming_client.INamingClient
|
||||
toolsDescription map[string]*registry.ToolDescription
|
||||
toolsRpcContext map[string]*registry.RpcContext
|
||||
toolChangeEventListeners []registry.ToolChangeEventListener
|
||||
currentServiceSet map[string]bool
|
||||
}
|
||||
|
||||
const DEFAULT_SERVICE_LIST_MAX_PGSIZXE = 10000
|
||||
const MCP_TOOL_SUBFIX = "-mcp-tools.json"
|
||||
|
||||
func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
|
||||
if n.toolsDescription == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
|
||||
result := []*registry.ToolDescription{}
|
||||
for _, tool := range n.toolsDescription {
|
||||
result = append(result, tool)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
|
||||
if n.toolsRpcContext == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
tool, ok := n.toolsRpcContext[toolName]
|
||||
return tool, ok
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
|
||||
n.toolChangeEventListeners = append(n.toolChangeEventListeners, listener)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsList() bool {
|
||||
changed := false
|
||||
for group, serviceMatcher := range n.serviceMatcher {
|
||||
if n.refreshToolsListForGroup(group, serviceMatcher) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
|
||||
services, err := n.namingClient.GetAllServicesInfo(vo.GetAllServiceInfoParam{
|
||||
GroupName: group,
|
||||
PageNo: 1,
|
||||
PageSize: DEFAULT_SERVICE_LIST_MAX_PGSIZXE,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Get service list error when refresh tools list for group %s, error %s", group, err))
|
||||
return false
|
||||
}
|
||||
|
||||
changed := false
|
||||
serviceList := services.Doms
|
||||
pattern, err := regexp.Compile(serviceMatcher)
|
||||
if err != nil {
|
||||
api.LogErrorf("Match service error for patter %s", serviceMatcher)
|
||||
return false
|
||||
}
|
||||
|
||||
currentServiceList := map[string]bool{}
|
||||
|
||||
for _, service := range serviceList {
|
||||
if !pattern.MatchString(service) {
|
||||
continue
|
||||
}
|
||||
|
||||
formatServiceName := getFormatServiceName(group, service)
|
||||
if _, ok := n.currentServiceSet[formatServiceName]; !ok {
|
||||
refreshed := n.refreshToolsListForService(group, service)
|
||||
n.listenToService(group, service)
|
||||
if refreshed {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
currentServiceList[formatServiceName] = true
|
||||
}
|
||||
|
||||
serviceShouldBeDeleted := []string{}
|
||||
for serviceName, _ := range n.currentServiceSet {
|
||||
if !strings.HasPrefix(serviceName, group) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := currentServiceList[serviceName]; !ok {
|
||||
serviceShouldBeDeleted = append(serviceShouldBeDeleted, serviceName)
|
||||
changed = true
|
||||
toolsShouldBeDeleted := []string{}
|
||||
for toolName, _ := range n.toolsDescription {
|
||||
if strings.HasPrefix(toolName, serviceName) {
|
||||
toolsShouldBeDeleted = append(toolsShouldBeDeleted, toolName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, toolName := range toolsShouldBeDeleted {
|
||||
delete(n.toolsDescription, toolName)
|
||||
delete(n.toolsRpcContext, toolName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, service := range serviceShouldBeDeleted {
|
||||
delete(n.currentServiceSet, service)
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
func getFormatServiceName(group string, service string) string {
|
||||
return fmt.Sprintf("%s_%s", group, service)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
|
||||
toolsNeedReset := []string{}
|
||||
|
||||
formatServiceName := getFormatServiceName(group, service)
|
||||
for tool, _ := range n.toolsDescription {
|
||||
if strings.HasPrefix(tool, formatServiceName) {
|
||||
toolsNeedReset = append(toolsNeedReset, tool)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tool := range toolsNeedReset {
|
||||
delete(n.toolsDescription, tool)
|
||||
delete(n.toolsRpcContext, tool)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
|
||||
|
||||
if newConfig == nil {
|
||||
dataId := makeToolsConfigId(service)
|
||||
content, err := n.configClient.GetConfig(vo.ConfigParam{
|
||||
DataId: dataId,
|
||||
Group: group,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Get tools config for sercice %s:%s error %s", group, service, err))
|
||||
return false
|
||||
}
|
||||
|
||||
newConfig = &content
|
||||
}
|
||||
|
||||
if instances == nil {
|
||||
instancesFromNacos, err := n.namingClient.SelectInstances(vo.SelectInstancesParam{
|
||||
ServiceName: service,
|
||||
GroupName: group,
|
||||
HealthyOnly: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("List instance for sercice %s:%s error %s", group, service, err))
|
||||
return false
|
||||
}
|
||||
|
||||
instances = &instancesFromNacos
|
||||
}
|
||||
|
||||
var applicationDescription registry.McpApplicationDescription
|
||||
if newConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// config deleted, tools should be removed
|
||||
if len(*newConfig) == 0 {
|
||||
n.deleteToolForService(group, service)
|
||||
return true
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(*newConfig), &applicationDescription)
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Parse tools config for sercice %s:%s error, config is %s, error is %s", group, service, *newConfig, err))
|
||||
return false
|
||||
}
|
||||
|
||||
wrappedInstances := []registry.Instance{}
|
||||
for _, instance := range *instances {
|
||||
wrappedInstance := registry.Instance{
|
||||
Host: instance.Ip,
|
||||
Port: instance.Port,
|
||||
Meta: instance.Metadata,
|
||||
}
|
||||
wrappedInstances = append(wrappedInstances, wrappedInstance)
|
||||
}
|
||||
|
||||
if n.toolsDescription == nil {
|
||||
n.toolsDescription = map[string]*registry.ToolDescription{}
|
||||
}
|
||||
|
||||
if n.toolsRpcContext == nil {
|
||||
n.toolsRpcContext = map[string]*registry.RpcContext{}
|
||||
}
|
||||
|
||||
n.deleteToolForService(group, service)
|
||||
|
||||
for _, tool := range applicationDescription.ToolsDescription {
|
||||
meta := applicationDescription.ToolsMeta[tool.Name]
|
||||
|
||||
var cred *registry.CredentialInfo
|
||||
credentialRef := meta.CredentialRef
|
||||
if credentialRef != nil {
|
||||
cred = n.GetCredential(*credentialRef, group)
|
||||
}
|
||||
|
||||
context := registry.RpcContext{
|
||||
ToolMeta: meta,
|
||||
Instances: &wrappedInstances,
|
||||
Protocol: applicationDescription.Protocol,
|
||||
Credential: cred,
|
||||
}
|
||||
|
||||
tool.Name = makeToolName(group, service, tool.Name)
|
||||
n.toolsDescription[tool.Name] = tool
|
||||
n.toolsRpcContext[tool.Name] = &context
|
||||
}
|
||||
n.currentServiceSet[getFormatServiceName(group, service)] = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.CredentialInfo {
|
||||
dataId := makeCredentialDataId(name)
|
||||
content, err := n.configClient.GetConfig(vo.ConfigParam{
|
||||
DataId: dataId,
|
||||
Group: group,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Get credentials for %s:%s error %s", group, dataId, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var credential registry.CredentialInfo
|
||||
err = json.Unmarshal([]byte(content), &credential)
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Parse credentials for %s:%s error %s", group, dataId, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &credential
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) bool {
|
||||
return n.refreshToolsListForServiceWithContent(group, service, nil, nil)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) listenToService(group string, service string) {
|
||||
|
||||
// config changed, tools description may be changed
|
||||
err := n.configClient.ListenConfig(vo.ConfigParam{
|
||||
DataId: makeToolsConfigId(service),
|
||||
Group: group,
|
||||
OnChange: func(namespace, group, dataId, data string) {
|
||||
n.refreshToolsListForServiceWithContent(group, service, &data, nil)
|
||||
for _, listener := range n.toolChangeEventListeners {
|
||||
listener.OnToolChanged(n)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Listen to service's tool config error %s", err))
|
||||
}
|
||||
|
||||
err = n.namingClient.Subscribe(&vo.SubscribeParam{
|
||||
ServiceName: service,
|
||||
GroupName: group,
|
||||
SubscribeCallback: func(services []model.Instance, err error) {
|
||||
n.refreshToolsListForServiceWithContent(group, service, nil, &services)
|
||||
for _, listener := range n.toolChangeEventListeners {
|
||||
listener.OnToolChanged(n)
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Listen to service's tool instance list error %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func makeToolName(group string, service string, toolName string) string {
|
||||
return fmt.Sprintf("%s_%s_%s", group, service, toolName)
|
||||
}
|
||||
|
||||
func makeToolsConfigId(service string) string {
|
||||
return service + MCP_TOOL_SUBFIX
|
||||
}
|
||||
|
||||
func makeCredentialDataId(credentialName string) string {
|
||||
return credentialName
|
||||
}
|
||||
184
plugins/golang-filter/mcp-server/registry/nacos/server.go
Normal file
184
plugins/golang-filter/mcp-server/registry/nacos/server.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package nacos
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/clients"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/vo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
|
||||
}
|
||||
|
||||
type NacosConfig struct {
|
||||
ServerAddr *string
|
||||
Ak *string
|
||||
Sk *string
|
||||
Namespace *string
|
||||
RegionId *string
|
||||
ServiceMatcher *map[string]string
|
||||
}
|
||||
|
||||
type McpServerToolsChangeListener struct {
|
||||
mcpServer *internal.MCPServer
|
||||
}
|
||||
|
||||
func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegistry) {
|
||||
resetToolsToMcpServer(l.mcpServer, reg)
|
||||
}
|
||||
|
||||
func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
|
||||
sc := []constant.ServerConfig{
|
||||
*constant.NewServerConfig(*config.ServerAddr, 8848, constant.WithContextPath("/nacos")),
|
||||
}
|
||||
|
||||
//create ClientConfig
|
||||
cc := *constant.NewClientConfig(
|
||||
constant.WithTimeoutMs(5000),
|
||||
constant.WithNotLoadCacheAtStart(true),
|
||||
constant.WithOpenKMS(true),
|
||||
constant.WithLogLevel("error"),
|
||||
)
|
||||
cc.AppendToStdout = true
|
||||
|
||||
cc.DiableLog = true
|
||||
|
||||
if config.Namespace != nil {
|
||||
cc.NamespaceId = *config.Namespace
|
||||
}
|
||||
|
||||
if config.RegionId != nil {
|
||||
cc.RegionId = *config.RegionId
|
||||
}
|
||||
|
||||
if config.Ak != nil {
|
||||
cc.AccessKey = *config.Ak
|
||||
}
|
||||
|
||||
if config.Sk != nil {
|
||||
cc.SecretKey = *config.Sk
|
||||
}
|
||||
|
||||
// create config client
|
||||
configClient, err := clients.NewConfigClient(
|
||||
vo.NacosClientParam{
|
||||
ClientConfig: &cc,
|
||||
ServerConfigs: sc,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initial nacos config client: %w", err)
|
||||
}
|
||||
|
||||
namingClient, err := clients.NewNamingClient(
|
||||
vo.NacosClientParam{
|
||||
ClientConfig: &cc,
|
||||
ServerConfigs: sc,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initial naming config client: %w", err)
|
||||
}
|
||||
|
||||
return &NacosMcpRegsitry{
|
||||
configClient: configClient,
|
||||
namingClient: namingClient,
|
||||
serviceMatcher: *config.ServiceMatcher,
|
||||
toolChangeEventListeners: []registry.ToolChangeEventListener{},
|
||||
currentServiceSet: map[string]bool{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *NacosConfig) ParseConfig(config map[string]any) error {
|
||||
|
||||
serverAddr, ok := config["serverAddr"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing serverAddr")
|
||||
}
|
||||
c.ServerAddr = &serverAddr
|
||||
|
||||
serviceMatcher, ok := config["serviceMatcher"].(map[string]any)
|
||||
if !ok {
|
||||
return errors.New("missing serviceMatcher")
|
||||
}
|
||||
|
||||
if namespace, ok := config["namespace"].(string); ok {
|
||||
c.Namespace = &namespace
|
||||
}
|
||||
|
||||
matchers := map[string]string{}
|
||||
for key, value := range serviceMatcher {
|
||||
matchers[key] = value.(string)
|
||||
}
|
||||
|
||||
c.ServiceMatcher = &matchers
|
||||
|
||||
if ak, ok := config["accessKey"].(string); ok {
|
||||
c.Ak = &ak
|
||||
}
|
||||
|
||||
if sk, ok := config["secretKey"].(string); ok {
|
||||
c.Sk = &sk
|
||||
}
|
||||
|
||||
if region, ok := config["regionId"].(string); ok {
|
||||
c.RegionId = ®ion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(
|
||||
serverName,
|
||||
"1.0.0",
|
||||
)
|
||||
|
||||
nacosRegistry, err := CreateNacosMcpRegsitry(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize NacosMcpRegistry: %w", err)
|
||||
}
|
||||
|
||||
listener := McpServerToolsChangeListener{
|
||||
mcpServer: mcpServer,
|
||||
}
|
||||
nacosRegistry.RegisterToolChangeEventListener(&listener)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("NacosToolsListRefresh recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
if nacosRegistry.refreshToolsList() {
|
||||
resetToolsToMcpServer(mcpServer, nacosRegistry)
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
}()
|
||||
return mcpServer, nil
|
||||
}
|
||||
|
||||
func resetToolsToMcpServer(mcpServer *internal.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []internal.ServerTool{}
|
||||
tools := reg.ListToolsDesciption()
|
||||
for _, tool := range tools {
|
||||
wrappedTools = append(wrappedTools, internal.ServerTool{
|
||||
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
|
||||
Handler: registry.HandleRegistryToolsCall(reg),
|
||||
})
|
||||
}
|
||||
mcpServer.SetTools(wrappedTools...)
|
||||
api.LogInfof("Tools reset, new tools list len %d", len(wrappedTools))
|
||||
}
|
||||
64
plugins/golang-filter/mcp-server/registry/registry.go
Normal file
64
plugins/golang-filter/mcp-server/registry/registry.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type McpApplicationDescription struct {
|
||||
Protocol string `json:"protocol"`
|
||||
ToolsDescription []*ToolDescription `json:"tools"`
|
||||
ToolsMeta map[string]ToolMeta `json:"toolsMeta"`
|
||||
}
|
||||
|
||||
type ToolMeta struct {
|
||||
InvokeContext map[string]string `json:"invokeContext"`
|
||||
ParametersMapping map[string]ParameterMapInfo `json:"parametersMapping"`
|
||||
CredentialRef *string `json:"credentialRef"`
|
||||
}
|
||||
|
||||
type ParameterMapInfo struct {
|
||||
ParamName string `json:"name"`
|
||||
BackendName string `json:"backendName"`
|
||||
ParamType string `json:"type"`
|
||||
Position string `json:"position"`
|
||||
}
|
||||
|
||||
type ToolDescription struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema json.RawMessage `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type ToolChangeEventListener interface {
|
||||
OnToolChanged(McpServerRegistry)
|
||||
}
|
||||
|
||||
type McpServerRegistry interface {
|
||||
ListToolsDesciption() []*ToolDescription
|
||||
GetToolRpcContext(toolname string) (*RpcContext, bool)
|
||||
RegisterToolChangeEventListener(listener ToolChangeEventListener)
|
||||
}
|
||||
|
||||
type RpcContext struct {
|
||||
Instances *[]Instance
|
||||
ToolMeta ToolMeta
|
||||
Protocol string
|
||||
Credential *CredentialInfo
|
||||
}
|
||||
|
||||
type CredentialInfo struct {
|
||||
CredentialType string `json:"type"`
|
||||
Credentials map[string]any `json:"credentialsMap"`
|
||||
}
|
||||
|
||||
type Instance struct {
|
||||
Host string
|
||||
Port uint64
|
||||
Meta map[string]string
|
||||
}
|
||||
|
||||
type RemoteCallHandle interface {
|
||||
HandleToolCall(ctx *RpcContext, parameters map[string]any) (*mcp.CallToolResult, error)
|
||||
}
|
||||
212
plugins/golang-filter/mcp-server/registry/remote.go
Normal file
212
plugins/golang-filter/mcp-server/registry/remote.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const HTTP_URL_TEMPLATE = "%s://%s:%d%s"
|
||||
const FIX_QUERY_TOKEN_KEY = "key"
|
||||
const FIX_QUERY_TOKEN_VALUE = "value"
|
||||
const PROTOCOL_HTTP = "http"
|
||||
const PROTOCOL_HTTPS = "https"
|
||||
const DEFAULT_HTTP_METHOD = "GET"
|
||||
const DEFAULT_HTTP_PATH = "/"
|
||||
|
||||
func getHttpCredentialHandle(name string) (func(*CredentialInfo, *HttpRemoteCallHandle), error) {
|
||||
if name == "fixed-query-token" {
|
||||
return FixedQueryToken, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unknown credential type")
|
||||
}
|
||||
|
||||
type CommonRemoteCallHandle struct {
|
||||
Instance *Instance
|
||||
}
|
||||
|
||||
type HttpRemoteCallHandle struct {
|
||||
CommonRemoteCallHandle
|
||||
Protocol string
|
||||
Headers http.Header
|
||||
Body *string
|
||||
Query map[string]string
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
// http credentials handles
|
||||
func FixedQueryToken(cred *CredentialInfo, h *HttpRemoteCallHandle) {
|
||||
key, _ := cred.Credentials[FIX_QUERY_TOKEN_KEY]
|
||||
value, _ := cred.Credentials[FIX_QUERY_TOKEN_VALUE]
|
||||
h.Query[key.(string)] = value.(string)
|
||||
}
|
||||
|
||||
func newHttpRemoteCallHandle(ctx *RpcContext) (*HttpRemoteCallHandle, error) {
|
||||
instance, err := selectOneInstance(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
method, ok := ctx.ToolMeta.InvokeContext["method"]
|
||||
if !ok {
|
||||
method = DEFAULT_HTTP_METHOD
|
||||
}
|
||||
|
||||
path, ok := ctx.ToolMeta.InvokeContext["path"]
|
||||
if !ok {
|
||||
path = DEFAULT_HTTP_PATH
|
||||
}
|
||||
|
||||
return &HttpRemoteCallHandle{
|
||||
CommonRemoteCallHandle: CommonRemoteCallHandle{
|
||||
Instance: instance,
|
||||
},
|
||||
Protocol: ctx.Protocol,
|
||||
Headers: http.Header{},
|
||||
Body: nil,
|
||||
Query: map[string]string{},
|
||||
Path: path,
|
||||
Method: method,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// http remote handle implementation
|
||||
func (h *HttpRemoteCallHandle) HandleToolCall(ctx *RpcContext, parameters map[string]any) (*mcp.CallToolResult, error) {
|
||||
if ctx.Credential != nil {
|
||||
credentialHandle, err := getHttpCredentialHandle(ctx.Credential.CredentialType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
credentialHandle(ctx.Credential, h)
|
||||
}
|
||||
|
||||
err := h.handleParamMapping(&ctx.ToolMeta.ParametersMapping, parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := h.doHttpCall()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseType := "text"
|
||||
if respType, ok := ctx.ToolMeta.InvokeContext["responseType"]; ok {
|
||||
responseType = respType
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: responseType,
|
||||
Text: string(body),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *HttpRemoteCallHandle) handleParamMapping(mapInfo *map[string]ParameterMapInfo, params map[string]any) error {
|
||||
paramMapInfo := *mapInfo
|
||||
for param, value := range params {
|
||||
if info, ok := paramMapInfo[param]; ok {
|
||||
if info.Position == "Query" {
|
||||
h.Query[info.BackendName] = fmt.Sprintf("%v", value)
|
||||
} else if info.Position == "Header" {
|
||||
h.Headers[info.BackendName] = []string{fmt.Sprintf("%v", value)}
|
||||
} else {
|
||||
return fmt.Errorf("Unsupport position for args %s, pos is %s", param, info.Position)
|
||||
}
|
||||
} else {
|
||||
h.Query[param] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HttpRemoteCallHandle) doHttpCall() (*http.Response, error) {
|
||||
pathPrefix := fmt.Sprintf(HTTP_URL_TEMPLATE, h.Protocol, h.Instance.Host, h.Instance.Port, h.Path)
|
||||
queryString := ""
|
||||
queryGroup := []string{}
|
||||
for queryKey, queryValue := range h.Query {
|
||||
queryGroup = append(queryGroup, url.QueryEscape(queryKey)+"="+url.QueryEscape(queryValue))
|
||||
}
|
||||
|
||||
if len(queryGroup) > 0 {
|
||||
queryString = "?" + strings.Join(queryGroup, "&")
|
||||
}
|
||||
fullUrl, err := url.Parse(pathPrefix + queryString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Parse url error , url is %s", pathPrefix+queryString)
|
||||
}
|
||||
request := http.Request{
|
||||
URL: fullUrl,
|
||||
Method: h.Method,
|
||||
Header: h.Headers,
|
||||
}
|
||||
|
||||
if h.Body != nil {
|
||||
request.Body = io.NopCloser(strings.NewReader(*h.Body))
|
||||
}
|
||||
|
||||
return http.DefaultClient.Do(&request)
|
||||
}
|
||||
|
||||
func selectOneInstance(ctx *RpcContext) (*Instance, error) {
|
||||
instanceId := 0
|
||||
if ctx.Instances == nil || len(*ctx.Instances) == 0 {
|
||||
return nil, fmt.Errorf("No instance")
|
||||
}
|
||||
|
||||
instances := *ctx.Instances
|
||||
if len(instances) > 1 {
|
||||
instanceId = rand.Intn(len(instances) - 1)
|
||||
}
|
||||
select_instance := instances[instanceId]
|
||||
return &select_instance, nil
|
||||
}
|
||||
|
||||
func getRemoteCallhandle(ctx *RpcContext) (RemoteCallHandle, error) {
|
||||
if ctx.Protocol == PROTOCOL_HTTP || ctx.Protocol == PROTOCOL_HTTPS {
|
||||
return newHttpRemoteCallHandle(ctx)
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// common remote call process
|
||||
func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[string]any) (*mcp.CallToolResult, error) {
|
||||
ctx, ok := reg.GetToolRpcContext(toolName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unknown tool %s", toolName)
|
||||
}
|
||||
|
||||
remoteHandle, err := getRemoteCallhandle(ctx)
|
||||
if remoteHandle == nil {
|
||||
return nil, fmt.Errorf("Unknown backend protocol %s", ctx.Protocol)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Call backend server error: %w", err)
|
||||
}
|
||||
|
||||
return remoteHandle.HandleToolCall(ctx, parameters)
|
||||
}
|
||||
|
||||
func HandleRegistryToolsCall(reg McpServerRegistry) internal.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
return CommonRemoteCall(reg, request.Params.Name, arguments)
|
||||
}
|
||||
}
|
||||
209
plugins/golang-filter/mcp-server/servers/gorm/db.go
Normal file
209
plugins/golang-filter/mcp-server/servers/gorm/db.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"gorm.io/driver/clickhouse"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// DBClient is a struct to handle database connections and operations
|
||||
type DBClient struct {
|
||||
db *gorm.DB
|
||||
dsn string
|
||||
dbType string
|
||||
reconnect chan struct{}
|
||||
stop chan struct{}
|
||||
panicCount int32 // Add panic counter
|
||||
}
|
||||
|
||||
// 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{
|
||||
dsn: dsn,
|
||||
dbType: dbType,
|
||||
reconnect: make(chan struct{}, 1),
|
||||
stop: stop,
|
||||
}
|
||||
|
||||
// Start reconnection goroutine
|
||||
go client.reconnectLoop()
|
||||
|
||||
// Try initial connection
|
||||
if err := client.connect(); err != nil {
|
||||
api.LogErrorf("Initial database connection failed: %v", err)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *DBClient) connect() error {
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
gormConfig := gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}
|
||||
|
||||
switch c.dbType {
|
||||
case "postgres":
|
||||
db, err = gorm.Open(postgres.Open(c.dsn), &gormConfig)
|
||||
case "clickhouse":
|
||||
db, err = gorm.Open(clickhouse.Open(c.dsn), &gormConfig)
|
||||
case "mysql":
|
||||
db, err = gorm.Open(mysql.Open(c.dsn), &gormConfig)
|
||||
case "sqlite":
|
||||
db, err = gorm.Open(sqlite.Open(c.dsn), &gormConfig)
|
||||
default:
|
||||
return fmt.Errorf("unsupported database type %s", c.dbType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
c.db = db
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DBClient) reconnectLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Recovered from panic in reconnectLoop: %v", r)
|
||||
|
||||
// Increment panic counter
|
||||
atomic.AddInt32(&c.panicCount, 1)
|
||||
|
||||
// If panic count exceeds threshold, stop trying to reconnect
|
||||
if atomic.LoadInt32(&c.panicCount) > 3 {
|
||||
api.LogErrorf("Too many panics in reconnectLoop, stopping reconnection attempts")
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for a while before restarting
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Restart the reconnect loop
|
||||
go c.reconnectLoop()
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second) // Try to reconnect every 30 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.stop:
|
||||
api.LogInfof("Database %s connection closed", c.dbType)
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.db == nil || c.Ping() != nil {
|
||||
if err := c.connect(); err != nil {
|
||||
api.LogErrorf("Database reconnection failed: %v", err)
|
||||
} else {
|
||||
api.LogInfof("Database reconnected successfully")
|
||||
// Reset panic count on successful connection
|
||||
atomic.StoreInt32(&c.panicCount, 0)
|
||||
}
|
||||
}
|
||||
case <-c.reconnect:
|
||||
if err := c.connect(); err != nil {
|
||||
api.LogErrorf("Database reconnection failed: %v", err)
|
||||
} else {
|
||||
api.LogInfof("Database reconnected successfully")
|
||||
// Reset panic count on successful connection
|
||||
atomic.StoreInt32(&c.panicCount, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if c.db == nil {
|
||||
// Trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("database is not connected, attempting to reconnect")
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(query, args...).Rows()
|
||||
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)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Get column names
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get columns: %w", err)
|
||||
}
|
||||
|
||||
// Prepare a slice to hold the results
|
||||
var results []map[string]interface{}
|
||||
|
||||
// Iterate over the rows
|
||||
for rows.Next() {
|
||||
// Create a slice of interface{}'s to represent each column,
|
||||
// and a second slice to contain pointers to each item in the columns slice.
|
||||
columnsData := make([]interface{}, len(columns))
|
||||
columnsPointers := make([]interface{}, len(columns))
|
||||
for i := range columnsData {
|
||||
columnsPointers[i] = &columnsData[i]
|
||||
}
|
||||
|
||||
// Scan the result into the column pointers...
|
||||
if err := rows.Scan(columnsPointers...); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan row: %w", err)
|
||||
}
|
||||
|
||||
// Create a map to hold the column name and value
|
||||
rowMap := make(map[string]interface{})
|
||||
for i, colName := range columns {
|
||||
val := columnsData[i]
|
||||
b, ok := val.([]byte)
|
||||
if ok {
|
||||
rowMap[colName] = string(b)
|
||||
} else {
|
||||
rowMap[colName] = val
|
||||
}
|
||||
}
|
||||
|
||||
// Append the map to the results slice
|
||||
results = append(results, rowMap)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (c *DBClient) Ping() error {
|
||||
if c.db == nil {
|
||||
return fmt.Errorf("database connection is nil")
|
||||
}
|
||||
|
||||
// Use context to set timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Try to ping the database
|
||||
sqlDB, err := c.db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying *sql.DB: %v", err)
|
||||
}
|
||||
|
||||
return sqlDB.PingContext(ctx)
|
||||
}
|
||||
59
plugins/golang-filter/mcp-server/servers/gorm/server.go
Normal file
59
plugins/golang-filter/mcp-server/servers/gorm/server.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package gorm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("database", &DBConfig{})
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
dbType string
|
||||
dsn string
|
||||
description string
|
||||
}
|
||||
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
dsn, ok := config["dsn"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing dsn")
|
||||
}
|
||||
c.dsn = dsn
|
||||
|
||||
dbType, ok := config["dbType"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing database type")
|
||||
}
|
||||
c.dbType = dbType
|
||||
api.LogDebugf("DBConfig ParseConfig: %+v", config)
|
||||
c.description, ok = config["description"].(string)
|
||||
if !ok {
|
||||
c.description = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
|
||||
)
|
||||
|
||||
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
|
||||
// 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()),
|
||||
HandleQueryTool(dbClient),
|
||||
)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
55
plugins/golang-filter/mcp-server/servers/gorm/tools.go
Normal file
55
plugins/golang-filter/mcp-server/servers/gorm/tools.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// HandleQueryTool handles SQL query execution
|
||||
func HandleQueryTool(dbClient *DBClient) internal.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")
|
||||
}
|
||||
|
||||
results, err := dbClient.ExecuteSQL(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 &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(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The sql query to execute"
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -25,6 +26,8 @@
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/json_util.h"
|
||||
#include "http_util.h"
|
||||
|
||||
@@ -58,12 +61,15 @@ using ::Wasm::Common::JsonValueAs;
|
||||
template <typename PluginConfig>
|
||||
class RouteRuleMatcher {
|
||||
public:
|
||||
enum CATEGORY { Route, Host };
|
||||
enum CATEGORY { Route, RoutePrefix, Host, Service, RouteAndService };
|
||||
enum MATCH_TYPE { Prefix, Exact, Suffix };
|
||||
struct RuleConfig {
|
||||
CATEGORY category;
|
||||
std::unordered_set<std::string> routes;
|
||||
std::vector<std::string> route_prefixs;
|
||||
std::vector<std::pair<MATCH_TYPE, std::string>> hosts;
|
||||
std::unordered_set<std::string> services;
|
||||
bool disable = false;
|
||||
PluginConfig config;
|
||||
};
|
||||
struct AuthRuleConfig {
|
||||
@@ -88,6 +94,8 @@ class RouteRuleMatcher {
|
||||
return rules;
|
||||
}
|
||||
|
||||
bool globalAuthDisable() { return global_auth_ && !global_auth_.value(); }
|
||||
|
||||
FilterHeadersStatus onHeaders(
|
||||
const std::function<FilterHeadersStatus(const PluginConfig&)> process) {
|
||||
if (invalid_config_) {
|
||||
@@ -126,10 +134,10 @@ class RouteRuleMatcher {
|
||||
LOG_DEBUG("no match config");
|
||||
return true;
|
||||
}
|
||||
if (!config.second && global_auth_ && !global_auth_.value()) {
|
||||
// No allow set, means no need to check auth if global_auth is false
|
||||
if (!config.second && globalAuthDisable()) {
|
||||
// No allow set, means no need to check auth if global auth is disable
|
||||
LOG_DEBUG(
|
||||
"no allow set found, and global auth is false, no need to auth");
|
||||
"no allow set found, and global auth is disable, no need to auth");
|
||||
return true;
|
||||
}
|
||||
return checkPlugin(config.first.value(), config.second);
|
||||
@@ -154,27 +162,71 @@ class RouteRuleMatcher {
|
||||
auto request_host = request_host_header->view();
|
||||
std::string route_name;
|
||||
getValue({"route_name"}, &route_name);
|
||||
std::string service_name;
|
||||
getValue({"cluster_name"}, &service_name);
|
||||
std::optional<std::reference_wrapper<PluginConfig>> match_config;
|
||||
int rule_id;
|
||||
if (global_config_) {
|
||||
rule_id = 0;
|
||||
match_config = global_config_.value();
|
||||
}
|
||||
bool disable_rule = false;
|
||||
for (int i = 0; i < rule_config_.size(); ++i) {
|
||||
auto& rule = rule_config_[i];
|
||||
if (rule.category == CATEGORY::Host) {
|
||||
if (hostMatch(rule, request_host)) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
disable_rule = rule.disable;
|
||||
break;
|
||||
}
|
||||
} else if (rule.category == CATEGORY::Route) {
|
||||
// category == Route
|
||||
if (rule.routes.find(route_name) != rule.routes.end()) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
disable_rule = rule.disable;
|
||||
break;
|
||||
}
|
||||
} else if (rule.category == CATEGORY::RouteAndService) {
|
||||
// category == RouteAndService
|
||||
if (rule.routes.find(route_name) != rule.routes.end()) {
|
||||
if (serviceMatch(rule, service_name)) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
disable_rule = rule.disable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (rule.category == CATEGORY::Service) {
|
||||
// category == Service
|
||||
if (serviceMatch(rule, service_name)) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
disable_rule = rule.disable;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// category == RoutePrefix
|
||||
bool is_matched = false;
|
||||
for (auto& route_prefix : rule.route_prefixs) {
|
||||
if (route_name.length() < route_prefix.length() ||
|
||||
route_name.compare(0, route_prefix.length(), route_prefix) != 0) {
|
||||
continue;
|
||||
}
|
||||
is_matched = true;
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
disable_rule = rule.disable;
|
||||
break;
|
||||
}
|
||||
if (is_matched) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if (rule.routes.find(route_name) != rule.routes.end()) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (disable_rule) {
|
||||
return std::make_pair(-1, std::nullopt);
|
||||
}
|
||||
if (match_config) {
|
||||
return std::make_pair(rule_id, match_config);
|
||||
@@ -190,6 +242,8 @@ class RouteRuleMatcher {
|
||||
auto request_host = request_host_header->view();
|
||||
std::string route_name;
|
||||
getValue({"route_name"}, &route_name);
|
||||
std::string service_name;
|
||||
getValue({"service_name"}, &service_name);
|
||||
std::optional<std::reference_wrapper<PluginConfig>> match_config;
|
||||
std::optional<std::reference_wrapper<std::unordered_set<std::string>>>
|
||||
allow_set;
|
||||
@@ -200,33 +254,99 @@ class RouteRuleMatcher {
|
||||
return std::make_pair(match_config, std::nullopt);
|
||||
}
|
||||
bool is_matched = false;
|
||||
bool disable_rule = false;
|
||||
for (auto& auth_rule : auth_rule_config_) {
|
||||
if (auth_rule.rule_config.category == CATEGORY::Host) {
|
||||
if (hostMatch(auth_rule.rule_config, request_host)) {
|
||||
LOG_DEBUG(absl::StrFormat("host %s is matched for this request",
|
||||
request_host));
|
||||
is_matched = true;
|
||||
if (auth_rule.has_local_config) {
|
||||
LOG_DEBUG("has local config");
|
||||
if (auth_rule.rule_config.disable) {
|
||||
disable_rule = true;
|
||||
} else if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
LOG_DEBUG("has not local config");
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if (auth_rule.rule_config.routes.find(route_name) !=
|
||||
auth_rule.rule_config.routes.end()) {
|
||||
is_matched = true;
|
||||
if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
allow_set = auth_rule.allow_set;
|
||||
} else if (auth_rule.rule_config.category == CATEGORY::Route) {
|
||||
// category == Route
|
||||
if (auth_rule.rule_config.routes.find(route_name) !=
|
||||
auth_rule.rule_config.routes.end()) {
|
||||
LOG_DEBUG(absl::StrFormat("route %s is matched for this request",
|
||||
route_name));
|
||||
is_matched = true;
|
||||
if (auth_rule.rule_config.disable) {
|
||||
disable_rule = true;
|
||||
} else if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (auth_rule.rule_config.category == CATEGORY::RouteAndService) {
|
||||
// category == RouteAndService
|
||||
if (auth_rule.rule_config.routes.find(route_name) !=
|
||||
auth_rule.rule_config.routes.end()) {
|
||||
LOG_DEBUG(absl::StrFormat("route %s is matched for this request",
|
||||
route_name));
|
||||
if (serviceMatch(auth_rule.rule_config, service_name)) {
|
||||
LOG_DEBUG(absl::StrFormat("service %s is matched for this request",
|
||||
service_name));
|
||||
is_matched = true;
|
||||
if (auth_rule.rule_config.disable) {
|
||||
disable_rule = true;
|
||||
} else if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (auth_rule.rule_config.category == CATEGORY::Service) {
|
||||
// category == Service
|
||||
if (serviceMatch(auth_rule.rule_config, service_name)) {
|
||||
LOG_DEBUG(absl::StrFormat("service %s is matched for this request",
|
||||
service_name));
|
||||
is_matched = true;
|
||||
if (auth_rule.rule_config.disable) {
|
||||
disable_rule = true;
|
||||
} else if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// category == RoutePrefix
|
||||
for (auto& route_prefix : auth_rule.rule_config.route_prefixs) {
|
||||
if (route_name.length() < route_prefix.length() ||
|
||||
route_name.compare(0, route_prefix.length(), route_prefix) != 0) {
|
||||
continue;
|
||||
}
|
||||
LOG_DEBUG(absl::StrFormat(
|
||||
"route_prefix %s is matched for this request", route_prefix));
|
||||
is_matched = true;
|
||||
if (auth_rule.rule_config.disable) {
|
||||
disable_rule = true;
|
||||
} else if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (is_matched) {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return is_matched || (global_auth_ && global_auth_.value())
|
||||
return !disable_rule &&
|
||||
(is_matched || (global_auth_ && global_auth_.value()))
|
||||
? std::make_pair(match_config, allow_set)
|
||||
: std::make_pair(std::nullopt, std::nullopt);
|
||||
}
|
||||
@@ -265,23 +385,48 @@ class RouteRuleMatcher {
|
||||
LOG_WARN("failed to parse configuration for _match_route_");
|
||||
return false;
|
||||
}
|
||||
if (!parseRoutePrefixMatchConfig(config, rule.route_prefixs)) {
|
||||
LOG_WARN("failed to parse configuration for _match_route_prefix_");
|
||||
return false;
|
||||
}
|
||||
if (!parseDomainMatchConfig(config, rule.hosts)) {
|
||||
LOG_WARN("failed to parse configuration for _match_domain_");
|
||||
return false;
|
||||
}
|
||||
auto no_route = rule.routes.empty();
|
||||
auto no_host = rule.hosts.empty();
|
||||
if ((no_route && no_host) || (!no_route && !no_host)) {
|
||||
if (!parseServiceMatchConfig(config, rule.services)) {
|
||||
LOG_WARN("failed to parse configuration for _match_service_");
|
||||
return false;
|
||||
}
|
||||
auto has_route = !rule.routes.empty();
|
||||
auto has_route_prefix = !rule.route_prefixs.empty();
|
||||
auto has_service = !rule.services.empty();
|
||||
auto has_host = !rule.hosts.empty();
|
||||
if (has_route + has_route_prefix + has_host + has_service == 0) {
|
||||
LOG_WARN(
|
||||
"there is only one of '_match_route_' and '_match_domain_' can "
|
||||
"there is at least one of '_match_route_', '_match_domain_', "
|
||||
"'_match_route_prefix_' and '_match_service_' can "
|
||||
"present in configuration.");
|
||||
return false;
|
||||
}
|
||||
if (!no_route) {
|
||||
if (has_route) {
|
||||
rule.category = CATEGORY::Route;
|
||||
if (has_service) {
|
||||
rule.category = CATEGORY::RouteAndService;
|
||||
}
|
||||
} else if (has_route_prefix) {
|
||||
rule.category = CATEGORY::RoutePrefix;
|
||||
} else if (has_service) {
|
||||
rule.category = CATEGORY::Service;
|
||||
} else {
|
||||
rule.category = CATEGORY::Host;
|
||||
}
|
||||
auto has_disable = config.find("_disable_");
|
||||
if (has_disable != config.end()) {
|
||||
auto disable = JsonValueAs<bool>(has_disable.value());
|
||||
if (disable.second == Wasm::Common::JsonParserResultDetail::OK) {
|
||||
rule.disable = disable.first.value();
|
||||
}
|
||||
}
|
||||
rule_config_.push_back(std::move(rule));
|
||||
}
|
||||
return true;
|
||||
@@ -323,8 +468,11 @@ class RouteRuleMatcher {
|
||||
for (const auto& item : rules.items()) {
|
||||
AuthRuleConfig auth_rule;
|
||||
auto config = item.value();
|
||||
// ignore the '_match_route_' or '_match_domain_' field
|
||||
auto local_config_size = config.size() - 1;
|
||||
auto has_allow = config.find("allow");
|
||||
if (has_allow != config.end()) {
|
||||
local_config_size -= 1;
|
||||
LOG_DEBUG("has allow filed");
|
||||
if (!JsonArrayIterate(config, "allow", [&](const json& allow) -> bool {
|
||||
auto parse_result = JsonValueAs<std::string>(allow);
|
||||
@@ -332,10 +480,10 @@ class RouteRuleMatcher {
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!parse_result.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'allow' field in filter configuration.");
|
||||
"failed to parse 'allow' field in filter "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG(parse_result.first.value());
|
||||
auth_rule.allow_set.insert(parse_result.first.value());
|
||||
return true;
|
||||
})) {
|
||||
@@ -343,32 +491,61 @@ class RouteRuleMatcher {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!parsePluginConfig(config, auth_rule.rule_config.config)) {
|
||||
if (has_allow == config.end()) {
|
||||
LOG_WARN("parse rule's config failed");
|
||||
return false;
|
||||
auto has_disable = config.find("_disable_");
|
||||
if (has_disable != config.end()) {
|
||||
local_config_size -= 1;
|
||||
auto disable = JsonValueAs<bool>(has_disable.value());
|
||||
if (disable.second == Wasm::Common::JsonParserResultDetail::OK) {
|
||||
auth_rule.rule_config.disable = disable.first.value();
|
||||
}
|
||||
}
|
||||
if (local_config_size > 0) {
|
||||
if (!parsePluginConfig(config, auth_rule.rule_config.config)) {
|
||||
if (has_allow == config.end()) {
|
||||
LOG_WARN("parse rule's config failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
auth_rule.has_local_config = true;
|
||||
}
|
||||
} else {
|
||||
auth_rule.has_local_config = true;
|
||||
}
|
||||
if (!parseRouteMatchConfig(config, auth_rule.rule_config.routes)) {
|
||||
LOG_WARN("failed to parse configuration for _match_route_");
|
||||
return false;
|
||||
}
|
||||
if (!parseRoutePrefixMatchConfig(config,
|
||||
auth_rule.rule_config.route_prefixs)) {
|
||||
LOG_WARN("failed to parse configuration for _match_route_prefix_");
|
||||
return false;
|
||||
}
|
||||
if (!parseServiceMatchConfig(config, auth_rule.rule_config.services)) {
|
||||
LOG_WARN("failed to parse configuration for _match_service_");
|
||||
return false;
|
||||
}
|
||||
if (!parseDomainMatchConfig(config, auth_rule.rule_config.hosts)) {
|
||||
LOG_WARN("failed to parse configuration for _match_domain_");
|
||||
return false;
|
||||
}
|
||||
auto no_route = auth_rule.rule_config.routes.empty();
|
||||
auto no_host = auth_rule.rule_config.hosts.empty();
|
||||
if ((no_route && no_host) || (!no_route && !no_host)) {
|
||||
auto has_route = !auth_rule.rule_config.routes.empty();
|
||||
auto has_route_prefix = !auth_rule.rule_config.route_prefixs.empty();
|
||||
auto has_host = !auth_rule.rule_config.hosts.empty();
|
||||
auto has_service = !auth_rule.rule_config.services.empty();
|
||||
if (has_route + has_route_prefix + has_host + has_service == 0) {
|
||||
LOG_WARN(
|
||||
"there is only one of '_match_route_' and '_match_domain_' can "
|
||||
"there is at least one of '_match_route_', '_match_domain_', "
|
||||
"'_match_route_prefix_' and '_match_service_' can "
|
||||
"present in configuration.");
|
||||
return false;
|
||||
}
|
||||
if (!no_route) {
|
||||
if (has_route) {
|
||||
auth_rule.rule_config.category = CATEGORY::Route;
|
||||
if (has_service) {
|
||||
auth_rule.rule_config.category = CATEGORY::RouteAndService;
|
||||
}
|
||||
} else if (has_route_prefix) {
|
||||
auth_rule.rule_config.category = CATEGORY::RoutePrefix;
|
||||
} else if (has_service) {
|
||||
auth_rule.rule_config.category = CATEGORY::Service;
|
||||
} else {
|
||||
auth_rule.rule_config.category = CATEGORY::Host;
|
||||
}
|
||||
@@ -419,6 +596,27 @@ class RouteRuleMatcher {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool serviceMatch(const RuleConfig& rule, std::string_view request_service) {
|
||||
if (rule.services.empty()) {
|
||||
// If no services specified, consider this rule applies to all host.
|
||||
return true;
|
||||
}
|
||||
std::vector<std::string> result = absl::StrSplit(request_service, '|');
|
||||
if (result.size() != 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string port = result[1];
|
||||
std::string fqdn = result[3];
|
||||
|
||||
for (const std::string& service_match : rule.services) {
|
||||
if (service_match == fqdn || service_match == fqdn + ":" + port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parseRouteMatchConfig(const json& config,
|
||||
std::unordered_set<std::string>& routes) {
|
||||
return JsonArrayIterate(
|
||||
@@ -436,6 +634,23 @@ class RouteRuleMatcher {
|
||||
});
|
||||
}
|
||||
|
||||
bool parseRoutePrefixMatchConfig(const json& config,
|
||||
std::vector<std::string>& route_prefixs) {
|
||||
return JsonArrayIterate(
|
||||
config, "_match_route_prefix_", [&](const json& route) -> bool {
|
||||
auto parse_result = JsonValueAs<std::string>(route);
|
||||
if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!parse_result.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse '_match_route_prefix_' field in filter "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
route_prefixs.emplace_back(parse_result.first.value());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool parseDomainMatchConfig(
|
||||
const json& config,
|
||||
std::vector<std::pair<MATCH_TYPE, std::string>>& hosts) {
|
||||
@@ -475,6 +690,23 @@ class RouteRuleMatcher {
|
||||
});
|
||||
}
|
||||
|
||||
bool parseServiceMatchConfig(const json& config,
|
||||
std::unordered_set<std::string>& services) {
|
||||
return JsonArrayIterate(
|
||||
config, "_match_service_", [&](const json& service) -> bool {
|
||||
auto parse_result = JsonValueAs<std::string>(service);
|
||||
if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!parse_result.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse '_match_service_' field in filter "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
services.insert(parse_result.first.value());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool invalid_config_ = false;
|
||||
std::optional<bool> global_auth_ = std::nullopt;
|
||||
std::vector<RuleConfig> rule_config_;
|
||||
|
||||
@@ -294,7 +294,12 @@ bool PluginRootContext::checkConsumer(
|
||||
}
|
||||
auto key_to_name_iter = rule.key_to_name.find(std::string(ca_key));
|
||||
if (key_to_name_iter != rule.key_to_name.end()) {
|
||||
if (allow_set && !allow_set.value().empty()) {
|
||||
if (allow_set) {
|
||||
if (allow_set.value().empty()) {
|
||||
LOG_DEBUG("allow set is empty, nobody is allowed");
|
||||
deniedUnauthorizedConsumer();
|
||||
return false;
|
||||
}
|
||||
if (allow_set.value().find(key_to_name_iter->second) ==
|
||||
allow_set.value().end()) {
|
||||
LOG_DEBUG(absl::StrCat("consumer is not allowed: ",
|
||||
@@ -435,6 +440,7 @@ FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto config = rootCtx->getMatchAuthConfig();
|
||||
config_ = config.first;
|
||||
if (!config_) {
|
||||
LOG_DEBUG("no matched config found");
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
allow_set_ = config.second;
|
||||
|
||||
@@ -624,6 +624,41 @@ TEST_F(HmacAuthTest, TimestampSecCheck) {
|
||||
EXPECT_EQ(context_->onRequestBody(0, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, EmptyAllowSet) {
|
||||
headers_ = {
|
||||
{":path", "/Third/Tools/checkSign"},
|
||||
{":method", "GET"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/json"},
|
||||
{"x-ca-timestamp", "1646365291734"},
|
||||
{"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="},
|
||||
};
|
||||
HmacAuthConfigRule rule;
|
||||
rule.credentials = {{"appKey", "appSecret"}};
|
||||
// EXPECT_EQ(root_context_->checkPlugin(rule, std::nullopt), true);
|
||||
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [{"key": "appKey", "secret": "appSecret", "name": "consumer"}],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_prefix_":["test"],
|
||||
"allow":[]
|
||||
}
|
||||
]
|
||||
})";
|
||||
route_name_ = "test@op1";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopAllIterationAndBuffer);
|
||||
}
|
||||
|
||||
} // namespace hmac_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
@@ -45,15 +45,13 @@ namespace {
|
||||
const std::string OriginalAuthKey("X-HI-ORIGINAL-AUTH");
|
||||
|
||||
void deniedInvalidCredentials(const std::string& realm) {
|
||||
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key",
|
||||
"",
|
||||
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}});
|
||||
}
|
||||
|
||||
void deniedUnauthorizedConsumer(const std::string& realm) {
|
||||
sendLocalResponse(
|
||||
403, "Request denied by Key Auth check. Unauthorized consumer", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
|
||||
sendLocalResponse(403, "Request denied by Key Auth check. Unauthorized consumer", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -62,12 +60,16 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
KeyAuthConfigRule& rule) {
|
||||
if ((configuration.find("consumers") != configuration.end()) &&
|
||||
(configuration.find("credentials") != configuration.end())) {
|
||||
LOG_WARN(
|
||||
"The consumers field and the credentials field cannot appear at the "
|
||||
"same level");
|
||||
LOG_WARN("The consumers field and the credentials field cannot appear at the same level");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
if ((configuration.find("consumers") == configuration.end()) &&
|
||||
(configuration.find("credentials") == configuration.end())) {
|
||||
LOG_WARN("No consumers and no credentials");
|
||||
return false;
|
||||
}
|
||||
if (configuration.find("credentials") != configuration.end()) {
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "credentials", [&](const json& credentials) -> bool {
|
||||
auto credential = JsonValueAs<std::string>(credentials);
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
@@ -76,34 +78,93 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
rule.credentials.insert(credential.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
|
||||
auto key = JsonValueAs<std::string>(item);
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for keys.");
|
||||
return false;
|
||||
}
|
||||
if (rule.keys.empty()) {
|
||||
LOG_WARN("at least one key has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(OriginalAuthKey);
|
||||
auto it = configuration.find("realm");
|
||||
if (it != configuration.end()) {
|
||||
auto realm_string = JsonValueAs<std::string>(it.value());
|
||||
if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
rule.realm = realm_string.first.value();
|
||||
}
|
||||
it = configuration.find("in_query");
|
||||
if (it != configuration.end()) {
|
||||
auto in_query = JsonValueAs<bool>(it.value());
|
||||
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_query.first) {
|
||||
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_query = in_query.first.value();
|
||||
}
|
||||
it = configuration.find("in_header");
|
||||
if (it != configuration.end()) {
|
||||
auto in_header = JsonValueAs<bool>(it.value());
|
||||
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_header = in_header.first.value();
|
||||
}
|
||||
if (!rule.in_query && !rule.in_header) {
|
||||
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
|
||||
return false;
|
||||
}
|
||||
// LOG_DEBUG(rule.debugString("parse phase, credentials branch"));
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
Consumer c;
|
||||
auto item = consumer.find("name");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'name' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto name = JsonValueAs<std::string>(item.value());
|
||||
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!name.first) {
|
||||
return false;
|
||||
}
|
||||
c.name = name.first.value();
|
||||
item = consumer.find("credential");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'credential' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
if (configuration.find("consumers") != configuration.end()) {
|
||||
bool need_global_keys = false;
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
Consumer c;
|
||||
auto item = consumer.find("name");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'name' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto name = JsonValueAs<std::string>(item.value());
|
||||
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!name.first) {
|
||||
return false;
|
||||
}
|
||||
c.name = name.first.value();
|
||||
if (consumer.find("credential") != consumer.end() &&
|
||||
consumer.find("credentials") != consumer.end()) {
|
||||
LOG_WARN("'credential' and 'credentials' can't appear at the same time.");
|
||||
return false;
|
||||
}
|
||||
if (consumer.find("credential") == consumer.end() &&
|
||||
consumer.find("credentials") == consumer.end()) {
|
||||
LOG_WARN("at least one of 'credential' and 'credentials' should be set.");
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("credential");
|
||||
if (item != consumer.end()) {
|
||||
auto credential = JsonValueAs<std::string>(item.value());
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!credential.first) {
|
||||
return false;
|
||||
}
|
||||
c.credential = credential.first.value();
|
||||
c.credentials.insert(credential.first.value());
|
||||
if (rule.credential_to_name.find(credential.first.value()) !=
|
||||
rule.credential_to_name.end()) {
|
||||
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
|
||||
@@ -113,106 +174,118 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
rule.credentials.insert(credential.first.value());
|
||||
rule.credential_to_name.emplace(
|
||||
std::make_pair(credential.first.value(), name.first.value()));
|
||||
item = consumer.find("keys");
|
||||
if (item != consumer.end()) {
|
||||
c.keys = std::vector<std::string>{OriginalAuthKey};
|
||||
}
|
||||
item = consumer.find("credentials");
|
||||
if (item != consumer.end()) {
|
||||
if (!JsonArrayIterate(
|
||||
consumer, "keys", [&](const json& key_json) -> bool {
|
||||
auto key = JsonValueAs<std::string>(key_json);
|
||||
if (key.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
c.keys->push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for consumer keys.");
|
||||
consumer, "credentials", [&](const json& credential_json) -> bool {
|
||||
auto credential = JsonValueAs<std::string>(credential_json);
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!credential.first) {
|
||||
return false;
|
||||
}
|
||||
c.credentials.insert(credential.first.value());
|
||||
if (rule.credential_to_name.find(credential.first.value()) !=
|
||||
rule.credential_to_name.end()) {
|
||||
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
|
||||
credential.first.value()));
|
||||
return false;
|
||||
}
|
||||
rule.credentials.insert(credential.first.value());
|
||||
rule.credential_to_name.emplace(
|
||||
std::make_pair(credential.first.value(), name.first.value()));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN(absl::StrCat("failed to parse credentials for consumer: ", c.name));
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("in_query");
|
||||
if (item != consumer.end()) {
|
||||
auto in_query = JsonValueAs<bool>(item.value());
|
||||
if (in_query.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_query.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'in_query' field in consumer "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
c.in_query = in_query.first;
|
||||
}
|
||||
item = consumer.find("in_header");
|
||||
if (item != consumer.end()) {
|
||||
auto in_header = JsonValueAs<bool>(item.value());
|
||||
if (in_header.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'in_header' field in consumer "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
c.in_header = in_header.first;
|
||||
}
|
||||
}
|
||||
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");
|
||||
need_global_keys = true;
|
||||
} else {
|
||||
c.keys = std::vector<std::string>{OriginalAuthKey};
|
||||
if (!JsonArrayIterate(
|
||||
consumer, "keys", [&](const json& key_json) -> bool {
|
||||
auto key = JsonValueAs<std::string>(key_json);
|
||||
if (key.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
c.keys->push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for consumer keys.");
|
||||
return false;
|
||||
}
|
||||
rule.consumers.push_back(std::move(c));
|
||||
item = consumer.find("in_query");
|
||||
if (item != consumer.end()) {
|
||||
auto in_query = JsonValueAs<bool>(item.value());
|
||||
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK || !in_query.first) {
|
||||
LOG_WARN("failed to parse 'in_query' field in consumer configuration.");
|
||||
return false;
|
||||
}
|
||||
c.in_query = in_query.first;
|
||||
}
|
||||
item = consumer.find("in_header");
|
||||
if (item != consumer.end()) {
|
||||
auto in_header = JsonValueAs<bool>(item.value());
|
||||
if (in_header.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'in_header' field in consumer "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
c.in_header = in_header.first;
|
||||
}
|
||||
}
|
||||
rule.consumers.push_back(std::move(c));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (need_global_keys) {
|
||||
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
|
||||
auto key = JsonValueAs<std::string>(item);
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
// if (rule.credentials.empty()) {
|
||||
// LOG_INFO("at least one credential has to be configured for a rule.");
|
||||
// return false;
|
||||
// }
|
||||
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
|
||||
auto key = JsonValueAs<std::string>(item);
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("failed to parse configuration for keys.");
|
||||
return false;
|
||||
}
|
||||
auto it = configuration.find("in_query");
|
||||
if (it != configuration.end()) {
|
||||
auto in_query = JsonValueAs<bool>(it.value());
|
||||
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_query.first) {
|
||||
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for keys.");
|
||||
return false;
|
||||
}
|
||||
if (rule.keys.empty()) {
|
||||
LOG_WARN("at least one key has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(OriginalAuthKey);
|
||||
auto it = configuration.find("realm");
|
||||
if (it != configuration.end()) {
|
||||
auto realm_string = JsonValueAs<std::string>(it.value());
|
||||
if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
rule.in_query = in_query.first.value();
|
||||
}
|
||||
it = configuration.find("in_header");
|
||||
if (it != configuration.end()) {
|
||||
auto in_header = JsonValueAs<bool>(it.value());
|
||||
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_header = in_header.first.value();
|
||||
}
|
||||
if (!rule.in_query && !rule.in_header) {
|
||||
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
rule.realm = realm_string.first.value();
|
||||
}
|
||||
it = configuration.find("in_query");
|
||||
if (it != configuration.end()) {
|
||||
auto in_query = JsonValueAs<bool>(it.value());
|
||||
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_query.first) {
|
||||
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_query = in_query.first.value();
|
||||
}
|
||||
it = configuration.find("in_header");
|
||||
if (it != configuration.end()) {
|
||||
auto in_header = JsonValueAs<bool>(it.value());
|
||||
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_header = in_header.first.value();
|
||||
}
|
||||
if (!rule.in_query && !rule.in_header) {
|
||||
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
|
||||
return false;
|
||||
// LOG_DEBUG(rule.debugString("parse phase, consumers branch"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -220,6 +293,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
bool PluginRootContext::checkPlugin(
|
||||
const KeyAuthConfigRule& rule,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set) {
|
||||
// LOG_DEBUG(rule.debugString("check phase"));
|
||||
if (rule.consumers.empty()) {
|
||||
for (const auto& key : rule.keys) {
|
||||
auto credential = extractCredential(rule.in_header, rule.in_query, key);
|
||||
@@ -263,9 +337,8 @@ bool PluginRootContext::checkPlugin(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (credential != consumer.credential) {
|
||||
LOG_DEBUG("credential does not match the consumer's credential: " +
|
||||
credential);
|
||||
if (consumer.credentials.find(credential) == consumer.credentials.end()) {
|
||||
LOG_DEBUG("credential " + credential + " does not match the consumer " + consumer.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -277,9 +350,13 @@ bool PluginRootContext::checkPlugin(
|
||||
|
||||
auto credential_to_name_iter = rule.credential_to_name.find(credential);
|
||||
if (credential_to_name_iter != rule.credential_to_name.end()) {
|
||||
if (allow_set && !allow_set->empty()) {
|
||||
if (allow_set->find(credential_to_name_iter->second) ==
|
||||
allow_set->end()) {
|
||||
if (allow_set) {
|
||||
if (allow_set->empty()) {
|
||||
LOG_DEBUG("allow set is empty, nobody is allowed");
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
return false;
|
||||
}
|
||||
if (allow_set->find(credential_to_name_iter->second) == allow_set->end()) {
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
LOG_DEBUG("unauthorized consumer: " +
|
||||
credential_to_name_iter->second);
|
||||
|
||||
@@ -38,10 +38,26 @@ namespace key_auth {
|
||||
|
||||
struct Consumer {
|
||||
std::string name;
|
||||
std::string credential;
|
||||
std::unordered_set<std::string> credentials;
|
||||
std::optional<std::vector<std::string>> keys;
|
||||
std::optional<bool> in_query = std::nullopt;
|
||||
std::optional<bool> in_header = std::nullopt;
|
||||
|
||||
// std::string debugString() const {
|
||||
// std::string msg;
|
||||
// msg += "name: " + name + "\n";
|
||||
// msg += " keys: \n";
|
||||
// if (keys.has_value()) {
|
||||
// for (const auto& item : keys.value()) {
|
||||
// msg += " - " + item + "\n";
|
||||
// }
|
||||
// }
|
||||
// msg += " credentials: \n";
|
||||
// for (const auto& item : credentials) {
|
||||
// msg += " - " + item + "\n";
|
||||
// }
|
||||
// return msg;
|
||||
// }
|
||||
};
|
||||
|
||||
struct KeyAuthConfigRule {
|
||||
@@ -52,6 +68,30 @@ struct KeyAuthConfigRule {
|
||||
std::vector<std::string> keys;
|
||||
bool in_query = true;
|
||||
bool in_header = true;
|
||||
|
||||
// std::string debugString(std::string prompt="") const {
|
||||
// std::string msg;
|
||||
// msg += prompt + "\n";
|
||||
// msg += "realm: " + realm + "\n";
|
||||
// msg += "keys: \n";
|
||||
// for (const auto& item : keys) {
|
||||
// msg += "- " + item + "\n";
|
||||
// }
|
||||
// msg += "credentials: \n";
|
||||
// for (const auto& item : credentials) {
|
||||
// msg += "- " + item + "\n";
|
||||
// }
|
||||
// msg += "credential_to_name: \n";
|
||||
// for (const auto& item : credential_to_name) {
|
||||
// msg += "- " + item.first + ": " + item.second + "\n";
|
||||
// }
|
||||
// msg += "consumers: \n";
|
||||
// for (const auto& item : consumers) {
|
||||
// msg += "- " + item.debugString();
|
||||
// }
|
||||
|
||||
// return msg;
|
||||
// }
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
|
||||
@@ -148,6 +148,11 @@ TEST_F(KeyAuthTest, InQuery) {
|
||||
path_ = "/test?hello=123&apiKey=123&x-api-key=def";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "pass";
|
||||
path_ = "/pass?hello=123&apiKey=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, InQueryWithConsumer) {
|
||||
@@ -177,6 +182,35 @@ TEST_F(KeyAuthTest, InQueryWithConsumer) {
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, EmptyAllowSet) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [{"credential" : "abc", "name" : "consumer1"}],
|
||||
"keys" : [ "apiKey", "x-api-key" ],
|
||||
"_rules_" : [ {"_match_route_" : ["test"], "allow" : []}, {"_match_route_prefix_" : ["prefix"], "allow" : []} ]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=1&apiKey=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
route_name_ = "noauth";
|
||||
path_ = "/test?hello=1&apiKey=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "prefix@operation";
|
||||
path_ = "/test?hello=1";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, EmptyConsumer) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
@@ -301,6 +335,131 @@ TEST_F(KeyAuthTest, ConsumerDifferentKey) {
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, ConsumerMultiCredentials) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"global_auth": false,
|
||||
"consumers": [
|
||||
{
|
||||
"name": "c1",
|
||||
"credentials":["123","345"],
|
||||
"keys": ["c1key"],
|
||||
"in_header": false,
|
||||
"in_query": true
|
||||
},
|
||||
{
|
||||
"name": "c2",
|
||||
"credentials":["abc","def"],
|
||||
"keys": ["c2key"],
|
||||
"in_header": false,
|
||||
"in_query": true
|
||||
}
|
||||
],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["test"],
|
||||
"allow": ["c1"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?c1key=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?c2key=adc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, ConsumerDefaultKey) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"global_auth": false,
|
||||
"consumers": [
|
||||
{
|
||||
"name": "c1",
|
||||
"credentials":["123","345"],
|
||||
"keys": ["c1key"],
|
||||
"in_header": false,
|
||||
"in_query": true
|
||||
},
|
||||
{
|
||||
"name": "c2",
|
||||
"credentials":["abc","def"]
|
||||
}
|
||||
],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["test"],
|
||||
"allow": ["c2"]
|
||||
}
|
||||
],
|
||||
"keys": ["defaultkey"]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?c1key=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?defaultkey=def";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, NoGlobalKeySetting) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"global_auth": false,
|
||||
"consumers": [
|
||||
{
|
||||
"name": "c1",
|
||||
"credentials":["123","345"],
|
||||
"keys": ["c1key"],
|
||||
"in_header": false,
|
||||
"in_query": true
|
||||
},
|
||||
{
|
||||
"name": "c2",
|
||||
"credentials":["abc","def"],
|
||||
"keys": ["c2key"]
|
||||
}
|
||||
],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["test"],
|
||||
"allow": ["c2"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?c1key=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?c2key=def";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace key_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
@@ -44,8 +44,8 @@ static RegisterContextFactory register_ModelMapper(
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view SetDecoderBufferLimitKey =
|
||||
"SetRequestBodyBufferLimit";
|
||||
constexpr std::string_view DefaultMaxBodyBytes = "10485760";
|
||||
"set_decoder_buffer_limit";
|
||||
constexpr std::string_view DefaultMaxBodyBytes = "104857600";
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -166,6 +166,7 @@ FilterHeadersStatus PluginRootContext::onHeader(
|
||||
}
|
||||
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
|
||||
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
|
||||
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ struct ModelMapperConfigRule {
|
||||
std::map<std::string, std::string> exact_model_mapping_;
|
||||
std::vector<std::pair<std::string, std::string>> prefix_model_mapping_;
|
||||
std::string default_model_mapping_;
|
||||
std::vector<std::string> enable_on_path_suffix_ = {"/v1/chat/completions"};
|
||||
std::vector<std::string> enable_on_path_suffix_ = {
|
||||
"/completions", "/embeddings", "/images/generations",
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 功能说明
|
||||
## 功能说明
|
||||
`model-router`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
# 配置字段
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
|
||||
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
|
||||
## 运行属性
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
## Function Description
|
||||
The `model-router` plugin implements the function of routing based on the model parameter in the LLM protocol.
|
||||
## Feature Description
|
||||
The `model-router` plugin implements routing functionality based on the model parameter in LLM protocols.
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Name | Data Type | Filling Requirement | Default Value | Description |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | Optional | model | The location of the model parameter in the request body |
|
||||
| `addProviderHeader` | string | Optional | - | Which request header to place the provider name parsed from the model parameter |
|
||||
| `modelToHeader` | string | Optional | - | Which request header to directly place the model parameter |
|
||||
| `enableOnPathSuffix` | array of string | Optional | ["/v1/chat/completions"] | Only effective for requests with these specific path suffixes |
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `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 |
|
||||
|
||||
## Runtime Attributes
|
||||
## Runtime Properties
|
||||
|
||||
Plugin execution phase: Authentication phase
|
||||
Plugin execution priority: 900
|
||||
|
||||
## Effect Description
|
||||
|
||||
### Routing Based on the model Parameter
|
||||
### Routing Based on Model Parameter
|
||||
|
||||
The following configuration is required:
|
||||
The following configuration is needed:
|
||||
|
||||
```yaml
|
||||
modelToHeader: x-higress-llm-model
|
||||
```
|
||||
|
||||
The plugin will extract the model parameter from the request and set it in the x-higress-llm-model request header, which can be used for subsequent routing. For example, the original LLM request body:
|
||||
The plugin extracts the model parameter from the request and sets it to the x-higress-llm-model request header for subsequent routing. For example, the original LLM request body is:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -35,7 +35,7 @@ The plugin will extract the model parameter from the request and set it in the x
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project"
|
||||
"content": "What is the GitHub address of the Higress project's main repository?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
@@ -43,21 +43,21 @@ The plugin will extract the model parameter from the request and set it in the x
|
||||
}
|
||||
```
|
||||
|
||||
After processing by this plugin, the following request header (which can be used for route matching) will be added:
|
||||
After processing by this plugin, the following request header will be added (can be used for route matching):
|
||||
|
||||
x-higress-llm-model: qwen-long
|
||||
|
||||
### Extracting the provider Field from the model Parameter for Routing
|
||||
### Extracting Provider Field from Model Parameter for Routing
|
||||
|
||||
> Note that this mode requires the client to specify the provider using a `/` separator in the model parameter.
|
||||
> Note that this mode requires the client to specify the provider in the model parameter using the `/` delimiter
|
||||
|
||||
The following configuration is required:
|
||||
The following configuration is needed:
|
||||
|
||||
```yaml
|
||||
addProviderHeader: x-higress-llm-provider
|
||||
```
|
||||
|
||||
The plugin will extract the provider part (if present) from the model parameter in the request and set it in the x-higress-llm-provider request header, which can be used for subsequent routing, and rewrite the model parameter to the model name part. For example, the original LLM request body:
|
||||
The plugin extracts the provider part (if any) from the model parameter in the request, sets it to the x-higress-llm-provider request header for subsequent routing, and rewrites the model parameter to only contain the model name part. For example, the original LLM request body is:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -67,7 +67,7 @@ The plugin will extract the provider part (if present) from the model parameter
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project"
|
||||
"content": "What is the GitHub address of the Higress project's main repository?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
@@ -75,7 +75,7 @@ The plugin will extract the provider part (if present) from the model parameter
|
||||
}
|
||||
```
|
||||
|
||||
After processing by this plugin, the following request header (which can be used for route matching) will be added:
|
||||
After processing by this plugin, the following request header will be added (can be used for route matching):
|
||||
|
||||
x-higress-llm-provider: dashscope
|
||||
|
||||
@@ -89,7 +89,7 @@ The original LLM request body will be changed to:
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "What is the GitHub address of the main repository for the higress project"
|
||||
"content": "What is the GitHub address of the Higress project's main repository?"
|
||||
}],
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
|
||||
@@ -44,8 +44,8 @@ static RegisterContextFactory register_ModelRouter(
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view SetDecoderBufferLimitKey =
|
||||
"SetRequestBodyBufferLimit";
|
||||
constexpr std::string_view DefaultMaxBodyBytes = "10485760";
|
||||
"set_decoder_buffer_limit";
|
||||
constexpr std::string_view DefaultMaxBodyBytes = "104857600";
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -137,6 +137,11 @@ FilterHeadersStatus PluginRootContext::onHeader(
|
||||
}
|
||||
bool enable = false;
|
||||
for (const auto& enable_suffix : rule.enable_on_path_suffix_) {
|
||||
// Support wildcard "*" to enable for all paths
|
||||
if (enable_suffix == "*") {
|
||||
enable = true;
|
||||
break;
|
||||
}
|
||||
if (absl::EndsWith({path.c_str(), uri_end}, enable_suffix)) {
|
||||
enable = true;
|
||||
break;
|
||||
@@ -153,6 +158,7 @@ FilterHeadersStatus PluginRootContext::onHeader(
|
||||
}
|
||||
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
|
||||
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
|
||||
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ struct ModelRouterConfigRule {
|
||||
std::string model_key_ = "model";
|
||||
std::string add_provider_header_;
|
||||
std::string model_to_header_;
|
||||
std::vector<std::string> enable_on_path_suffix_ = {"/v1/chat/completions"};
|
||||
std::vector<std::string> enable_on_path_suffix_ = {
|
||||
"/completions", "/embeddings", "/images/generations",
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
|
||||
@@ -17,7 +17,14 @@ COPY . .
|
||||
WORKDIR /workspace/extensions/$PLUGIN_NAME
|
||||
|
||||
RUN go mod tidy
|
||||
RUN tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./
|
||||
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
|
||||
|
||||
FROM scratch as output
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@ COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${COMMIT_ID})
|
||||
IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
|
||||
GOPROXY := $(shell go env GOPROXY)
|
||||
EXTRA_TAGS ?=
|
||||
EXTRA_TAGS := $(shell [ -f extensions/${PLUGIN_NAME}/.buildrc ] && . extensions/${PLUGIN_NAME}/.buildrc && echo $$EXTRA_TAGS || echo "")
|
||||
|
||||
.DEFAULT:
|
||||
build:
|
||||
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=${PLUGIN_NAME} \
|
||||
--build-arg BUILDER=${BUILDER} \
|
||||
--build-arg GOPROXY=$(GOPROXY) \
|
||||
--build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
|
||||
--build-arg EXTRA_TAGS=${EXTRA_TAGS} \
|
||||
-t ${IMG} \
|
||||
--output extensions/${PLUGIN_NAME} \
|
||||
.
|
||||
@@ -30,7 +30,7 @@ build-image:
|
||||
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=${PLUGIN_NAME} \
|
||||
--build-arg BUILDER=${BUILDER} \
|
||||
--build-arg GOPROXY=$(GOPROXY) \
|
||||
--build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
|
||||
--build-arg EXTRA_TAGS=${EXTRA_TAGS} \
|
||||
-t ${IMG} \
|
||||
.
|
||||
@echo ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user