mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 21:21:01 +08:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2c2d1d521 | ||
|
|
a5a28aebf6 | ||
|
|
1c10f36369 | ||
|
|
7054f01a36 | ||
|
|
895f17f8d8 | ||
|
|
29fcd330d5 | ||
|
|
0e58042fa6 | ||
|
|
bdbfad8a8a | ||
|
|
4307f88645 | ||
|
|
25b085cb5e | ||
|
|
dcea483c61 | ||
|
|
8fa1224cba | ||
|
|
8f7c10ee5f | ||
|
|
5a854b990b | ||
|
|
dd11248e47 | ||
|
|
ba98f3a7ad | ||
|
|
d31c978ed3 | ||
|
|
daa374d9a4 | ||
|
|
6b9dabb489 | ||
|
|
6f04404edd | ||
|
|
04a9104062 | ||
|
|
564f8c770a | ||
|
|
fec2e9dfc9 | ||
|
|
dc4ddb52ee | ||
|
|
6f221ead53 | ||
|
|
53f8410843 | ||
|
|
a17ac9e4c6 | ||
|
|
5e95f6f057 | ||
|
|
94f29e56c0 | ||
|
|
870157c576 | ||
|
|
c78ef7011d | ||
|
|
dc0dcaaaee | ||
|
|
34f5722d93 | ||
|
|
55fdddee2f | ||
|
|
980ffde244 | ||
|
|
0a578c2a04 | ||
|
|
536a3069a8 | ||
|
|
08c64ed467 | ||
|
|
cc74c0da93 | ||
|
|
210b97b06b | ||
|
|
bccfbde621 | ||
|
|
f1c6e78047 | ||
|
|
1c415c60c3 | ||
|
|
59acb61926 | ||
|
|
29079f4e2a | ||
|
|
95edce024d | ||
|
|
b6d07a157c | ||
|
|
10569f49ae | ||
|
|
2a588c99c7 | ||
|
|
0cfef34bff | ||
|
|
5c2b5d5750 | ||
|
|
8f483518a9 | ||
|
|
f6ee4ed166 | ||
|
|
9a9e924037 | ||
|
|
e7d66f691f | ||
|
|
8c48fcb423 | ||
|
|
ef31e09310 | ||
|
|
c0f2cafdc8 | ||
|
|
d5a9ff3a98 | ||
|
|
f069ad5b0d | ||
|
|
85219b6c53 | ||
|
|
5041277be3 | ||
|
|
c00c8827f9 | ||
|
|
46218058d1 | ||
|
|
5306385e6b | ||
|
|
4e881fdd3f | ||
|
|
59aa3b5488 | ||
|
|
c40cf85aad | ||
|
|
7c749b864c | ||
|
|
74ddbf02f6 | ||
|
|
60c56a16ab | ||
|
|
5a2c6835f7 | ||
|
|
12a5612450 | ||
|
|
b9f5c4d1f2 | ||
|
|
d7bdcbd026 | ||
|
|
dd284d1f24 | ||
|
|
a7ee523c98 | ||
|
|
4bbfb131ee | ||
|
|
6fd71f9749 | ||
|
|
e0159f501a | ||
|
|
56226d5052 | ||
|
|
086a9cc973 | ||
|
|
e389313aa3 | ||
|
|
f64c601264 | ||
|
|
9c6ea109f8 | ||
|
|
4ca2d23404 | ||
|
|
0ce52de59b | ||
|
|
81e459da01 |
114
.github/workflows/build-and-push-wasm-plugin-image.yaml
vendored
Normal file
114
.github/workflows/build-and-push-wasm-plugin-image.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Build and Push Wasm Plugin Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "wasm-go-*-v*.*.*" # 匹配 wasm-go-{pluginName}-vX.Y.Z 格式的标签
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
plugin_name:
|
||||
description: 'Name of the plugin'
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: 'Version of the plugin (optional, without leading v)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-and-push-wasm-plugin-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-msg
|
||||
env:
|
||||
IMAGE_REGISTRY_SERVICE: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
IMAGE_REPOSITORY: ${{ vars.PLUGIN_IMAGE_REPOSITORY || 'plugins' }}
|
||||
GO_VERSION: 1.19
|
||||
TINYGO_VERSION: 0.28.1
|
||||
ORAS_VERSION: 1.0.0
|
||||
steps:
|
||||
- name: Set plugin_name and version from inputs or ref_name
|
||||
id: set_vars
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
plugin_name="${{ github.event.inputs.plugin_name }}"
|
||||
version="${{ github.event.inputs.version }}"
|
||||
else
|
||||
ref_name=${{ github.ref_name }}
|
||||
plugin_name=${ref_name#*-*-} # 删除插件名前面的字段(wasm-go-)
|
||||
plugin_name=${plugin_name%-*} # 删除插件名后面的字段(-vX.Y.Z)
|
||||
version=$(echo "$ref_name" | awk -F'v' '{print $2}')
|
||||
fi
|
||||
|
||||
echo "PLUGIN_NAME=$plugin_name" >> $GITHUB_ENV
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: File Check
|
||||
run: |
|
||||
workspace=${{ github.workspace }}/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
push_command="./plugin.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
|
||||
# 查找spec.yaml
|
||||
if [ -f "${workspace}/spec.yaml" ]; then
|
||||
echo "spec.yaml exists"
|
||||
push_command="./spec.yaml:application/vnd.module.wasm.spec.v1+yaml $push_command "
|
||||
fi
|
||||
|
||||
# 查找README.md
|
||||
if [ -f "${workspace}/README.md" ];then
|
||||
echo "README.md exists"
|
||||
push_command="./README.md:application/vnd.module.wasm.doc.v1+markdown $push_command "
|
||||
fi
|
||||
|
||||
# 查找README_{lang}.md
|
||||
for file in ${workspace}/README_*.md; do
|
||||
if [ -f "$file" ]; then
|
||||
file_name=$(basename $file)
|
||||
echo "$file_name exists"
|
||||
lang=$(basename $file | sed 's/README_//; s/.md//')
|
||||
push_command="./$file_name:application/vnd.module.wasm.doc.v1.$lang+markdown $push_command "
|
||||
fi
|
||||
done
|
||||
|
||||
echo "PUSH_COMMAND=\"$push_command\"" >> $GITHUB_ENV
|
||||
|
||||
- name: Run a wasm-go-builder
|
||||
env:
|
||||
PLUGIN_NAME: ${{ env.PLUGIN_NAME }}
|
||||
BUILDER_IMAGE: higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-tinygo${{ env.TINYGO_VERSION }}-oras${{ env.ORAS_VERSION }}
|
||||
run: |
|
||||
docker run -itd --name builder -v ${{ github.workspace }}:/workspace -e PLUGIN_NAME=${{ env.PLUGIN_NAME }} --rm ${{ env.BUILDER_IMAGE }} /bin/bash
|
||||
|
||||
- name: Build Image and Push
|
||||
run: |
|
||||
push_command=${{ env.PUSH_COMMAND }}
|
||||
push_command=${push_command#\"}
|
||||
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号,确保oras push正常解析
|
||||
|
||||
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
|
||||
echo "TargetImage=${target_image}"
|
||||
|
||||
cd ${{ github.workspace }}/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
if [ -f ./.buildrc ]; then
|
||||
echo 'Found .buildrc file, sourcing it...'
|
||||
. ./.buildrc
|
||||
else
|
||||
echo '.buildrc file not found'
|
||||
fi
|
||||
echo "EXTRA_TAGS=${EXTRA_TAGS}"
|
||||
|
||||
command="
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
go mod tidy
|
||||
tinygo build -o ./plugin.wasm -scheduler=none -target=wasi -gc=custom -tags=\"custommalloc nottinygc_finalizer ${EXTRA_TAGS}\" .
|
||||
tar czvf plugin.tar.gz plugin.wasm
|
||||
echo ${{ secrets.REGISTRY_PASSWORD }} | oras login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ env.IMAGE_REGISTRY_SERVICE }}
|
||||
oras push ${target_image} ${push_command}
|
||||
"
|
||||
docker exec builder bash -c "$command"
|
||||
|
||||
|
||||
21
.github/workflows/build-and-test-plugin.yaml
vendored
21
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -17,8 +17,8 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
# There are too many lint errors in current code bases
|
||||
@@ -30,9 +30,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO(Xunzhuo): Enable C WASM Filters in CI
|
||||
wasmPluginType: [ GO ]
|
||||
wasmPluginType: [ GO, RUST ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
@@ -45,12 +45,17 @@ jobs:
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
if: matrix.wasmPluginType == 'RUST'
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -60,7 +65,7 @@ jobs:
|
||||
${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
.git/modules
|
||||
@@ -81,4 +86,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ higress-wasmplugin-test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
30
.github/workflows/build-and-test.yaml
vendored
30
.github/workflows/build-and-test.yaml
vendored
@@ -10,8 +10,8 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
# There are too many lint errors in current code bases
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
coverage-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
.git/modules
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Run Coverage Tests
|
||||
run: GOPROXY="https://proxy.golang.org,direct" make go.test.coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
files: ./coverage.xml
|
||||
@@ -58,17 +58,17 @@ jobs:
|
||||
needs: [lint,coverage-test]
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
.git/modules
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
run: GOPROXY="https://proxy.golang.org,direct" make build
|
||||
|
||||
- name: Upload Higress Binary
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: higress
|
||||
path: out/
|
||||
@@ -108,12 +108,12 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
.git/modules
|
||||
@@ -139,4 +139,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [higress-conformance-test,gateway-conformance-test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
36
.github/workflows/build-image-and-push.yaml
vendored
36
.github/workflows/build-image-and-push.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
envoy
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -107,12 +107,12 @@ jobs:
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
envoy
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -184,12 +184,12 @@ jobs:
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Setup Submodule Caches
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
envoy
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
|
||||
8
.github/workflows/codeql-analysis.yaml
vendored
8
.github/workflows/codeql-analysis.yaml
vendored
@@ -34,11 +34,11 @@ jobs:
|
||||
steps:
|
||||
# step 1
|
||||
- name: "Checkout repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# step 2: Initializes the CodeQL tools for scanning.
|
||||
- name: "Initialize CodeQL"
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: "Autobuild"
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# step 4
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
@@ -66,4 +66,4 @@ jobs:
|
||||
|
||||
# step 5
|
||||
- name: "Perform CodeQL Analysis"
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
# Step 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
# Step 2
|
||||
- id: package
|
||||
name: Prepare Standalone Package
|
||||
|
||||
2
.github/workflows/deploy-to-oss.yaml
vendored
2
.github/workflows/deploy-to-oss.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
# Step 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
# Step 2
|
||||
- name: Download Helm Charts Index
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
|
||||
4
.github/workflows/latest-release.yaml
vendored
4
.github/workflows/latest-release.yaml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
latest-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build hgctl latest multiarch binaries
|
||||
run: |
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
GITHUB_REPOSITORY: ${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
- name: Recreate the Latest Release and Tag
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: false
|
||||
prerelease: true
|
||||
|
||||
4
.github/workflows/license-checker.yaml
vendored
4
.github/workflows/license-checker.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
# step 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v4
|
||||
# step 2
|
||||
- name: Check License Header
|
||||
uses: apache/skywalking-eyes/header@25edfc2fd8d52fb266653fb5f6c42da633d85c07
|
||||
@@ -24,4 +24,4 @@ jobs:
|
||||
with:
|
||||
log: info
|
||||
config: .licenserc.yaml
|
||||
mode: check
|
||||
mode: check
|
||||
|
||||
6
.github/workflows/release-hgctl.yaml
vendored
6
.github/workflows/release-hgctl.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
env:
|
||||
HGCTL_VERSION: ${{github.ref_name}}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build hgctl latest multiarch binaries
|
||||
run: |
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
zip -q -r hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip out/windows_arm64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
@@ -34,4 +34,4 @@ jobs:
|
||||
hgctl_${{ env.HGCTL_VERSION }}_darwin_amd64.tar.gz
|
||||
hgctl_${{ env.HGCTL_VERSION }}_darwin_arm64.tar.gz
|
||||
hgctl_${{ env.HGCTL_VERSION }}_windows_amd64.zip
|
||||
hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip
|
||||
hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip
|
||||
|
||||
@@ -138,11 +138,11 @@ export ENVOY_TAR_PATH:=/home/package/envoy.tar.gz
|
||||
|
||||
external/package/envoy-amd64.tar.gz:
|
||||
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
|
||||
cd external/package; wget -O envoy-amd64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.0/envoy-symbol-amd64.tar.gz"
|
||||
cd external/package; wget -O envoy-amd64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.1/envoy-symbol-amd64.tar.gz"
|
||||
|
||||
external/package/envoy-arm64.tar.gz:
|
||||
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
|
||||
cd external/package; wget -O envoy-arm64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.0/envoy-symbol-arm64.tar.gz"
|
||||
cd external/package; wget -O envoy-arm64.tar.gz "https://github.com/alibaba/higress/releases/download/v1.4.1/envoy-symbol-arm64.tar.gz"
|
||||
|
||||
build-pilot:
|
||||
cd external/istio; rm -rf out/linux_amd64; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=amd64 BUILD_WITH_CONTAINER=1 make build-linux
|
||||
@@ -177,8 +177,8 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= sha-93966bf
|
||||
ISTIO_LATEST_IMAGE_TAG ?= sha-b00f79f
|
||||
ENVOY_LATEST_IMAGE_TAG ?= sha-59acb61
|
||||
ISTIO_LATEST_IMAGE_TAG ?= sha-59acb61
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
87
README.md
87
README.md
@@ -1,17 +1,18 @@
|
||||
<h1 align="center">
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
|
||||
<br>
|
||||
Cloud Native API Gateway
|
||||
AI Gateway
|
||||
</h1>
|
||||
<h4 align="center"> AI Native API Gateway </h4>
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
[**官网**](https://higress.io/) |
|
||||
[**文档**](https://higress.io/zh-cn/docs/overview/what-is-higress) |
|
||||
[**博客**](https://higress.io/zh-cn/blog) |
|
||||
[**开发指引**](https://higress.io/zh-cn/docs/developers/developers_dev) |
|
||||
[**Higress 企业版**](https://www.aliyun.com/product/aliware/mse?spm=higress-website.topbar.0.0.0)
|
||||
[**文档**](https://higress.io/docs/latest/user/quickstart/) |
|
||||
[**博客**](https://higress.io/blog/) |
|
||||
[**开发指引**](https://higress.io/docs/latest/dev/architecture/) |
|
||||
[**AI插件**](https://higress.io/plugin/)
|
||||
|
||||
|
||||
<p>
|
||||
@@ -19,21 +20,54 @@
|
||||
</p>
|
||||
|
||||
|
||||
Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。Higress 实现了安全防护网关、流量网关、微服务网关三层网关合一,可以显著降低网关的部署和运维成本。
|
||||
Higress 是基于阿里内部多年的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。
|
||||
|
||||
Higress 在阿里内部作为 AI 网关,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
|
||||
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
- [**快速开始**](#快速开始)
|
||||
- [**功能展示**](#功能展示)
|
||||
- [**使用场景**](#使用场景)
|
||||
- [**核心优势**](#核心优势)
|
||||
- [**Quick Start**](https://higress.io/zh-cn/docs/user/quickstart)
|
||||
- [**社区**](#社区)
|
||||
|
||||
## 快速开始
|
||||
|
||||
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
|
||||
|
||||
```bash
|
||||
# 创建一个工作目录
|
||||
mkdir higress; cd higress
|
||||
# 启动 higress,配置文件会写到工作目录下
|
||||
docker run -d --rm --name higress-ai -v ${PWD}:/data \
|
||||
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
|
||||
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
|
||||
```
|
||||
|
||||
监听端口说明如下:
|
||||
|
||||
- 8001 端口:Higress UI 控制台入口
|
||||
- 8080 端口:网关 HTTP 协议入口
|
||||
- 8443 端口:网关 HTTPS 协议入口
|
||||
|
||||
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内不可访问的影响**
|
||||
|
||||
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.io/docs/latest/user/quickstart/)。
|
||||
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **AI 网关**:
|
||||
|
||||
Higress 提供了一站式的 AI 插件集,可以增强依赖 AI 能力业务的稳定性、灵活性、可观测性,使得业务与 AI 的集成更加便捷和高效。
|
||||
|
||||
- **Kubernetes Ingress 网关**:
|
||||
|
||||
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
|
||||
@@ -56,27 +90,36 @@ Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源
|
||||
|
||||
脱胎于阿里巴巴2年多生产验证的内部产品,支持每秒请求量达数十万级的大规模场景。
|
||||
|
||||
彻底摆脱 reload 引起的流量抖动,配置变更毫秒级生效且业务无感。
|
||||
|
||||
- **平滑演进**
|
||||
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
|
||||
|
||||
支持 Nacos/Zookeeper/Eureka 等多种注册中心,可以不依赖 K8s Service 进行服务发现,支持非容器架构平滑演进到云原生架构。
|
||||
- **流式处理**
|
||||
|
||||
支持从 Nginx Ingress Controller 平滑迁移,支持平滑过渡到 Gateway API,支持业务架构平滑演进到 ServiceMesh。
|
||||
支持真正的完全流式处理请求/响应 Body,Wasm 插件很方便地自定义处理 SSE (Server-Sent Events)等流式协议的报文。
|
||||
|
||||
- **兼收并蓄**
|
||||
|
||||
兼容 Nginx Ingress Annotation 80%+ 的使用场景,且提供功能更丰富的 Higress Annotation 注解。
|
||||
|
||||
兼容 Ingress API/Gateway API/Istio API,可以组合多种 CRD 实现流量精细化管理。
|
||||
|
||||
在 AI 业务等大带宽场景下,可以显著降低内存开销。
|
||||
|
||||
- **便于扩展**
|
||||
|
||||
提供 Wasm、Lua、进程外三种插件扩展机制,支持多语言编写插件,生效粒度支持全局级、域名级,路由级。
|
||||
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能,满足90%以上的业务场景需求。
|
||||
|
||||
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
|
||||
|
||||
- **安全易用**
|
||||
|
||||
基于 Ingress API 和 Gateway API 标准,提供开箱即用的 UI 控制台,WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
|
||||
|
||||
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
|
||||
|
||||
插件支持热更新,变更插件逻辑和配置都对流量无损。
|
||||
|
||||
## 功能展示
|
||||
|
||||
### AI 网关 Demo 展示
|
||||
|
||||
[从 OpenAI 到其他大模型,30 秒完成迁移
|
||||
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
|
||||
|
||||
|
||||
### Higress UI 控制台
|
||||
|
||||
- **丰富的可观测**
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ type MatchRule struct {
|
||||
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
Config *types.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable bool `protobuf:"varint,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
@@ -367,6 +368,13 @@ func (m *MatchRule) GetConfigDisable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MatchRule) GetService() []string {
|
||||
if m != nil {
|
||||
return m.Service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterEnum("higress.extensions.v1alpha1.PluginPhase", PluginPhase_name, PluginPhase_value)
|
||||
proto.RegisterEnum("higress.extensions.v1alpha1.PullPolicy", PullPolicy_name, PullPolicy_value)
|
||||
@@ -377,46 +385,47 @@ func init() {
|
||||
func init() { proto.RegisterFile("extensions/v1alpha1/wasm.proto", fileDescriptor_4d60b240916c4e18) }
|
||||
|
||||
var fileDescriptor_4d60b240916c4e18 = []byte{
|
||||
// 619 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0xdd, 0x4e, 0xdb, 0x4c,
|
||||
0x10, 0x86, 0x71, 0x02, 0x81, 0x4c, 0x80, 0xcf, 0xac, 0xbe, 0xd2, 0x15, 0x54, 0x69, 0x84, 0xd4,
|
||||
0xd6, 0xe5, 0xc0, 0x16, 0xa1, 0x3f, 0x27, 0x15, 0x6a, 0x80, 0xb4, 0x44, 0x6d, 0x53, 0xcb, 0x86,
|
||||
0x56, 0xe5, 0xc4, 0xda, 0x98, 0x8d, 0xb3, 0xea, 0xfa, 0x47, 0xde, 0x35, 0x34, 0x17, 0xd2, 0x7b,
|
||||
0xea, 0x61, 0x2f, 0xa1, 0xe2, 0x2e, 0x7a, 0x56, 0x65, 0x6d, 0x43, 0x42, 0xab, 0x9c, 0xed, 0xce,
|
||||
0x3c, 0x33, 0xf3, 0xbe, 0xe3, 0x95, 0xa1, 0x49, 0xbf, 0x49, 0x1a, 0x09, 0x16, 0x47, 0xc2, 0xba,
|
||||
0xdc, 0x23, 0x3c, 0x19, 0x91, 0x3d, 0xeb, 0x8a, 0x88, 0xd0, 0x4c, 0xd2, 0x58, 0xc6, 0x68, 0x7b,
|
||||
0xc4, 0x82, 0x94, 0x0a, 0x61, 0xde, 0x72, 0x66, 0xc9, 0x6d, 0x35, 0x83, 0x38, 0x0e, 0x38, 0xb5,
|
||||
0x14, 0x3a, 0xc8, 0x86, 0xd6, 0x55, 0x4a, 0x92, 0x84, 0xa6, 0x22, 0x2f, 0xde, 0x7a, 0x70, 0x37,
|
||||
0x2f, 0x64, 0x9a, 0xf9, 0x32, 0xcf, 0xee, 0xfc, 0x5e, 0x04, 0xf8, 0x4c, 0x44, 0x68, 0xf3, 0x2c,
|
||||
0x60, 0x11, 0xd2, 0xa1, 0x9a, 0xa5, 0x1c, 0x57, 0x5a, 0x9a, 0x51, 0x77, 0x26, 0x47, 0xb4, 0x09,
|
||||
0x35, 0x31, 0x22, 0xed, 0xe7, 0x2f, 0x70, 0x55, 0x05, 0x8b, 0x1b, 0x72, 0x61, 0x83, 0x85, 0x24,
|
||||
0xa0, 0x5e, 0x92, 0x71, 0xee, 0x25, 0x31, 0x67, 0xfe, 0x18, 0x2f, 0xb6, 0x34, 0x63, 0xbd, 0xfd,
|
||||
0xc4, 0x9c, 0xa3, 0xd7, 0xb4, 0x33, 0xce, 0x6d, 0x85, 0x3b, 0xff, 0xa9, 0x0e, 0xb7, 0x01, 0xb4,
|
||||
0x3b, 0xd3, 0x54, 0x50, 0x3f, 0xa5, 0x12, 0x2f, 0xa9, 0xb9, 0xb7, 0xac, 0xab, 0xc2, 0xe8, 0x29,
|
||||
0xe8, 0x97, 0x34, 0x65, 0x43, 0xe6, 0x13, 0xc9, 0xe2, 0xc8, 0xfb, 0x4a, 0xc7, 0xb8, 0x96, 0xa3,
|
||||
0xd3, 0xf1, 0x77, 0x74, 0x8c, 0x5e, 0xc1, 0x5a, 0xa2, 0xfc, 0x79, 0x7e, 0x1c, 0x0d, 0x59, 0x80,
|
||||
0x97, 0x5b, 0x9a, 0xd1, 0x68, 0xdf, 0x37, 0xf3, 0xd5, 0x98, 0xe5, 0x6a, 0x4c, 0x57, 0xad, 0xc6,
|
||||
0x59, 0xcd, 0xe9, 0x23, 0x05, 0xa3, 0x87, 0xd0, 0x28, 0xaa, 0x23, 0x12, 0x52, 0xbc, 0xa2, 0x66,
|
||||
0x40, 0x1e, 0xea, 0x93, 0x90, 0xa2, 0x03, 0x58, 0x4a, 0x46, 0x44, 0x50, 0x5c, 0x57, 0xf6, 0x8d,
|
||||
0xf9, 0xf6, 0x55, 0x9d, 0x3d, 0xe1, 0x9d, 0xbc, 0x0c, 0xbd, 0x84, 0x95, 0x24, 0x65, 0x71, 0xca,
|
||||
0xe4, 0x18, 0x83, 0x52, 0xb6, 0xfd, 0x97, 0xb2, 0x5e, 0x24, 0xf7, 0xdb, 0x9f, 0x08, 0xcf, 0xa8,
|
||||
0x73, 0x03, 0xa3, 0x03, 0x58, 0xbf, 0xa0, 0x43, 0x92, 0x71, 0x59, 0x1a, 0xa3, 0xf3, 0x8d, 0xad,
|
||||
0x15, 0x78, 0xe1, 0xec, 0x2d, 0x34, 0x42, 0x22, 0xfd, 0x91, 0x97, 0x66, 0x9c, 0x0a, 0x3c, 0x6c,
|
||||
0x55, 0x8d, 0x46, 0xfb, 0xf1, 0x5c, 0xf9, 0x1f, 0x26, 0xbc, 0x93, 0x71, 0xea, 0x40, 0x58, 0x1e,
|
||||
0x05, 0x7a, 0x06, 0x9b, 0xb3, 0x42, 0xbc, 0x0b, 0x26, 0xc8, 0x80, 0x53, 0x1c, 0xb4, 0x34, 0x63,
|
||||
0xc5, 0xf9, 0x7f, 0x66, 0xee, 0x71, 0x9e, 0xdb, 0xf9, 0xae, 0x41, 0xfd, 0xa6, 0x1f, 0xc2, 0xb0,
|
||||
0xcc, 0x22, 0x35, 0x18, 0x6b, 0xad, 0xaa, 0x51, 0x77, 0xca, 0xeb, 0xe4, 0x09, 0x5e, 0xc4, 0x21,
|
||||
0x61, 0x11, 0xae, 0xa8, 0x44, 0x71, 0x43, 0x16, 0xd4, 0x0a, 0xdb, 0xd5, 0xf9, 0xb6, 0x0b, 0x0c,
|
||||
0x3d, 0x82, 0xf5, 0x3b, 0xf2, 0x16, 0x95, 0xbc, 0x35, 0x7f, 0x5a, 0xd7, 0x6e, 0x17, 0x1a, 0x53,
|
||||
0x5f, 0x09, 0xdd, 0x83, 0x8d, 0xb3, 0xbe, 0x6b, 0x77, 0x8f, 0x7a, 0x6f, 0x7a, 0xdd, 0x63, 0xcf,
|
||||
0x3e, 0xe9, 0xb8, 0x5d, 0x7d, 0x01, 0xd5, 0x61, 0xa9, 0x73, 0x76, 0x7a, 0xd2, 0xd7, 0xb5, 0xf2,
|
||||
0x78, 0xae, 0x57, 0x26, 0x47, 0xf7, 0xb4, 0x73, 0xea, 0xea, 0xd5, 0xdd, 0x43, 0x80, 0xa9, 0xa7,
|
||||
0xbd, 0x09, 0x68, 0xa6, 0xcb, 0xc7, 0xf7, 0xbd, 0xa3, 0x2f, 0xfa, 0x02, 0xd2, 0x61, 0xb5, 0x37,
|
||||
0xec, 0xc7, 0xd2, 0x4e, 0xa9, 0xa0, 0x91, 0xd4, 0x35, 0x04, 0x50, 0xeb, 0xf0, 0x2b, 0x32, 0x16,
|
||||
0x7a, 0xe5, 0xf0, 0xf5, 0x8f, 0xeb, 0xa6, 0xf6, 0xf3, 0xba, 0xa9, 0xfd, 0xba, 0x6e, 0x6a, 0xe7,
|
||||
0xed, 0x80, 0xc9, 0x51, 0x36, 0x30, 0xfd, 0x38, 0xb4, 0x08, 0x67, 0x03, 0x32, 0x20, 0x56, 0xf1,
|
||||
0xb1, 0x2c, 0x92, 0x30, 0xeb, 0x1f, 0xbf, 0x91, 0x41, 0x4d, 0x2d, 0x63, 0xff, 0x4f, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0xb9, 0xf2, 0x67, 0xbe, 0x64, 0x04, 0x00, 0x00,
|
||||
// 631 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0xdd, 0x6e, 0xd3, 0x4c,
|
||||
0x10, 0x86, 0xeb, 0xa4, 0x49, 0x9b, 0x49, 0xdb, 0xcf, 0x5d, 0x7d, 0x94, 0x55, 0x8b, 0x42, 0x54,
|
||||
0x09, 0x30, 0x3d, 0xb0, 0xd5, 0x94, 0x9f, 0x13, 0x54, 0x91, 0xb6, 0x81, 0x46, 0x40, 0xb0, 0xec,
|
||||
0x16, 0x44, 0x4f, 0xac, 0x8d, 0xbb, 0x71, 0x56, 0xac, 0x7f, 0xe4, 0x5d, 0xb7, 0xe4, 0xaa, 0xb8,
|
||||
0x0d, 0x0e, 0xb9, 0x04, 0xd4, 0xbb, 0xe0, 0x0c, 0x65, 0xed, 0x34, 0x49, 0x41, 0x39, 0xdb, 0x9d,
|
||||
0x79, 0x66, 0xe6, 0x7d, 0xc7, 0x2b, 0x43, 0x83, 0x7e, 0x93, 0x34, 0x12, 0x2c, 0x8e, 0x84, 0x75,
|
||||
0xb5, 0x4f, 0x78, 0x32, 0x24, 0xfb, 0xd6, 0x35, 0x11, 0xa1, 0x99, 0xa4, 0xb1, 0x8c, 0xd1, 0xce,
|
||||
0x90, 0x05, 0x29, 0x15, 0xc2, 0x9c, 0x72, 0xe6, 0x84, 0xdb, 0x6e, 0x04, 0x71, 0x1c, 0x70, 0x6a,
|
||||
0x29, 0xb4, 0x9f, 0x0d, 0xac, 0xeb, 0x94, 0x24, 0x09, 0x4d, 0x45, 0x5e, 0xbc, 0xfd, 0xe0, 0x6e,
|
||||
0x5e, 0xc8, 0x34, 0xf3, 0x65, 0x9e, 0xdd, 0xfd, 0xbd, 0x0c, 0xf0, 0x99, 0x88, 0xd0, 0xe6, 0x59,
|
||||
0xc0, 0x22, 0xa4, 0x43, 0x39, 0x4b, 0x39, 0x2e, 0x35, 0x35, 0xa3, 0xe6, 0x8c, 0x8f, 0x68, 0x0b,
|
||||
0xaa, 0x62, 0x48, 0x5a, 0xcf, 0x5f, 0xe0, 0xb2, 0x0a, 0x16, 0x37, 0xe4, 0xc2, 0x26, 0x0b, 0x49,
|
||||
0x40, 0xbd, 0x24, 0xe3, 0xdc, 0x4b, 0x62, 0xce, 0xfc, 0x11, 0x5e, 0x6e, 0x6a, 0xc6, 0x46, 0xeb,
|
||||
0x89, 0xb9, 0x40, 0xaf, 0x69, 0x67, 0x9c, 0xdb, 0x0a, 0x77, 0xfe, 0x53, 0x1d, 0xa6, 0x01, 0xb4,
|
||||
0x37, 0xd7, 0x54, 0x50, 0x3f, 0xa5, 0x12, 0x57, 0xd4, 0xdc, 0x29, 0xeb, 0xaa, 0x30, 0x7a, 0x0a,
|
||||
0xfa, 0x15, 0x4d, 0xd9, 0x80, 0xf9, 0x44, 0xb2, 0x38, 0xf2, 0xbe, 0xd2, 0x11, 0xae, 0xe6, 0xe8,
|
||||
0x6c, 0xfc, 0x1d, 0x1d, 0xa1, 0x57, 0xb0, 0x9e, 0x28, 0x7f, 0x9e, 0x1f, 0x47, 0x03, 0x16, 0xe0,
|
||||
0x95, 0xa6, 0x66, 0xd4, 0x5b, 0xf7, 0xcd, 0x7c, 0x35, 0xe6, 0x64, 0x35, 0xa6, 0xab, 0x56, 0xe3,
|
||||
0xac, 0xe5, 0xf4, 0xb1, 0x82, 0xd1, 0x43, 0xa8, 0x17, 0xd5, 0x11, 0x09, 0x29, 0x5e, 0x55, 0x33,
|
||||
0x20, 0x0f, 0xf5, 0x48, 0x48, 0xd1, 0x21, 0x54, 0x92, 0x21, 0x11, 0x14, 0xd7, 0x94, 0x7d, 0x63,
|
||||
0xb1, 0x7d, 0x55, 0x67, 0x8f, 0x79, 0x27, 0x2f, 0x43, 0x2f, 0x61, 0x35, 0x49, 0x59, 0x9c, 0x32,
|
||||
0x39, 0xc2, 0xa0, 0x94, 0xed, 0xfc, 0xa5, 0xac, 0x1b, 0xc9, 0x83, 0xd6, 0x27, 0xc2, 0x33, 0xea,
|
||||
0xdc, 0xc2, 0xe8, 0x10, 0x36, 0x2e, 0xe9, 0x80, 0x64, 0x5c, 0x4e, 0x8c, 0xd1, 0xc5, 0xc6, 0xd6,
|
||||
0x0b, 0xbc, 0x70, 0xf6, 0x16, 0xea, 0x21, 0x91, 0xfe, 0xd0, 0x4b, 0x33, 0x4e, 0x05, 0x1e, 0x34,
|
||||
0xcb, 0x46, 0xbd, 0xf5, 0x78, 0xa1, 0xfc, 0x0f, 0x63, 0xde, 0xc9, 0x38, 0x75, 0x20, 0x9c, 0x1c,
|
||||
0x05, 0x7a, 0x06, 0x5b, 0xf3, 0x42, 0xbc, 0x4b, 0x26, 0x48, 0x9f, 0x53, 0x1c, 0x34, 0x35, 0x63,
|
||||
0xd5, 0xf9, 0x7f, 0x6e, 0xee, 0x49, 0x9e, 0xdb, 0xfd, 0xae, 0x41, 0xed, 0xb6, 0x1f, 0xc2, 0xb0,
|
||||
0xc2, 0x22, 0x35, 0x18, 0x6b, 0xcd, 0xb2, 0x51, 0x73, 0x26, 0xd7, 0xf1, 0x13, 0xbc, 0x8c, 0x43,
|
||||
0xc2, 0x22, 0x5c, 0x52, 0x89, 0xe2, 0x86, 0x2c, 0xa8, 0x16, 0xb6, 0xcb, 0x8b, 0x6d, 0x17, 0x18,
|
||||
0x7a, 0x04, 0x1b, 0x77, 0xe4, 0x2d, 0x2b, 0x79, 0xeb, 0xfe, 0xac, 0xae, 0xb1, 0x12, 0x41, 0xd3,
|
||||
0x2b, 0xe6, 0x53, 0x5c, 0xc9, 0x95, 0x14, 0xd7, 0xbd, 0x0e, 0xd4, 0x67, 0xbe, 0x1f, 0xba, 0x07,
|
||||
0x9b, 0xe7, 0x3d, 0xd7, 0xee, 0x1c, 0x77, 0xdf, 0x74, 0x3b, 0x27, 0x9e, 0x7d, 0xda, 0x76, 0x3b,
|
||||
0xfa, 0x12, 0xaa, 0x41, 0xa5, 0x7d, 0x7e, 0x76, 0xda, 0xd3, 0xb5, 0xc9, 0xf1, 0x42, 0x2f, 0x8d,
|
||||
0x8f, 0xee, 0x59, 0xfb, 0xcc, 0xd5, 0xcb, 0x7b, 0x47, 0x00, 0x33, 0x8f, 0x7e, 0x0b, 0xd0, 0x5c,
|
||||
0x97, 0x8f, 0xef, 0xbb, 0xc7, 0x5f, 0xf4, 0x25, 0xa4, 0xc3, 0x5a, 0x77, 0xd0, 0x8b, 0xa5, 0x9d,
|
||||
0x52, 0x41, 0x23, 0xa9, 0x6b, 0x08, 0xa0, 0xda, 0xe6, 0xd7, 0x64, 0x24, 0xf4, 0xd2, 0xd1, 0xeb,
|
||||
0x1f, 0x37, 0x0d, 0xed, 0xe7, 0x4d, 0x43, 0xfb, 0x75, 0xd3, 0xd0, 0x2e, 0x5a, 0x01, 0x93, 0xc3,
|
||||
0xac, 0x6f, 0xfa, 0x71, 0x68, 0x11, 0xce, 0xfa, 0xa4, 0x4f, 0xac, 0xe2, 0x33, 0x5a, 0x24, 0x61,
|
||||
0xd6, 0x3f, 0x7e, 0x30, 0xfd, 0xaa, 0x5a, 0xd3, 0xc1, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0b,
|
||||
0x3c, 0xc3, 0xcf, 0x7e, 0x04, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *WasmPlugin) Marshal() (dAtA []byte, err error) {
|
||||
@@ -581,6 +590,15 @@ func (m *MatchRule) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if len(m.Service) > 0 {
|
||||
for iNdEx := len(m.Service) - 1; iNdEx >= 0; iNdEx-- {
|
||||
i -= len(m.Service[iNdEx])
|
||||
copy(dAtA[i:], m.Service[iNdEx])
|
||||
i = encodeVarintWasm(dAtA, i, uint64(len(m.Service[iNdEx])))
|
||||
i--
|
||||
dAtA[i] = 0x2a
|
||||
}
|
||||
}
|
||||
if m.ConfigDisable {
|
||||
i--
|
||||
if m.ConfigDisable {
|
||||
@@ -719,6 +737,12 @@ func (m *MatchRule) Size() (n int) {
|
||||
if m.ConfigDisable {
|
||||
n += 2
|
||||
}
|
||||
if len(m.Service) > 0 {
|
||||
for _, s := range m.Service {
|
||||
l = len(s)
|
||||
n += 1 + l + sovWasm(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
@@ -1291,6 +1315,38 @@ func (m *MatchRule) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
}
|
||||
m.ConfigDisable = bool(v != 0)
|
||||
case 5:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Service", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowWasm
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthWasm
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthWasm
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Service = append(m.Service, string(dAtA[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipWasm(dAtA[iNdEx:])
|
||||
|
||||
@@ -114,6 +114,7 @@ message MatchRule {
|
||||
repeated string domain = 2;
|
||||
google.protobuf.Struct config = 3;
|
||||
bool config_disable = 4;
|
||||
repeated string service = 5;
|
||||
}
|
||||
|
||||
// The phase in the filter chain where the plugin will be injected.
|
||||
|
||||
@@ -64,6 +64,10 @@ spec:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
service:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
type: array
|
||||
phase:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/source/common/http/headers.h b/source/common/http/headers.h
|
||||
index a7a8a3393e..6af4a2852d 100644
|
||||
--- a/source/common/http/headers.h
|
||||
+++ b/source/common/http/headers.h
|
||||
@@ -123,7 +123,7 @@ public:
|
||||
const LowerCaseString TriCostTime{"req-cost-time"};
|
||||
const LowerCaseString TriStartTime{"req-start-time"};
|
||||
const LowerCaseString TriRespStartTime{"resp-start-time"};
|
||||
- const LowerCaseString EnvoyOriginalHost{"original-host"};
|
||||
+ const LowerCaseString EnvoyOriginalHost{"x-envoy-original-host"};
|
||||
const LowerCaseString HigressOriginalService{"x-higress-original-service"};
|
||||
} AliExtendedValues;
|
||||
#endif
|
||||
43
envoy/1.20/patches/envoy/20240725-set-buffer-limit.patch
Normal file
43
envoy/1.20/patches/envoy/20240725-set-buffer-limit.patch
Normal file
@@ -0,0 +1,43 @@
|
||||
diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc
|
||||
index 9642d8abd3..410baa856f 100644
|
||||
--- a/source/extensions/common/wasm/context.cc
|
||||
+++ b/source/extensions/common/wasm/context.cc
|
||||
@@ -62,6 +62,21 @@ constexpr absl::string_view CelStateKeyPrefix = "wasm.";
|
||||
#if defined(ALIMESH)
|
||||
constexpr std::string_view ClearRouteCacheKey = "clear_route_cache";
|
||||
constexpr std::string_view DisableClearRouteCache = "off";
|
||||
+constexpr std::string_view SetDecoderBufferLimit = "set_decoder_buffer_limit";
|
||||
+constexpr std::string_view SetEncoderBufferLimit = "set_encoder_buffer_limit";
|
||||
+
|
||||
+bool stringViewToUint32(std::string_view str, uint32_t& out_value) {
|
||||
+ try {
|
||||
+ unsigned long temp = std::stoul(std::string(str));
|
||||
+ if (temp <= std::numeric_limits<uint32_t>::max()) {
|
||||
+ out_value = static_cast<uint32_t>(temp);
|
||||
+ return true;
|
||||
+ }
|
||||
+ } catch (const std::exception& e) {
|
||||
+ ENVOY_LOG_MISC(critical, "stringToUint exception '{}'", e.what());
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
#endif
|
||||
|
||||
using HashPolicy = envoy::config::route::v3::RouteAction::HashPolicy;
|
||||
@@ -1280,6 +1295,16 @@ WasmResult Context::setProperty(std::string_view path, std::string_view value) {
|
||||
} else {
|
||||
disable_clear_route_cache_ = false;
|
||||
}
|
||||
+ } else if (path == SetDecoderBufferLimit && decoder_callbacks_) {
|
||||
+ uint32_t buffer_limit;
|
||||
+ if (stringViewToUint32(value, buffer_limit)) {
|
||||
+ decoder_callbacks_->setDecoderBufferLimit(buffer_limit);
|
||||
+ }
|
||||
+ } else if (path == SetEncoderBufferLimit && encoder_callbacks_) {
|
||||
+ uint32_t buffer_limit;
|
||||
+ if (stringViewToUint32(value, buffer_limit)) {
|
||||
+ encoder_callbacks_->setEncoderBufferLimit(buffer_limit);
|
||||
+ }
|
||||
}
|
||||
#endif
|
||||
if (!state->setValue(toAbslStringView(value))) {
|
||||
106
envoy/1.20/patches/envoy/20240726-custom-span-tag.patch
Normal file
106
envoy/1.20/patches/envoy/20240726-custom-span-tag.patch
Normal file
@@ -0,0 +1,106 @@
|
||||
diff --git a/envoy/stream_info/stream_info.h b/envoy/stream_info/stream_info.h
|
||||
index c6d82db4f4..09717673b0 100644
|
||||
--- a/envoy/stream_info/stream_info.h
|
||||
+++ b/envoy/stream_info/stream_info.h
|
||||
@@ -613,7 +613,21 @@ public:
|
||||
* @return the number of times the request was attempted upstream, absl::nullopt if the request
|
||||
* was never attempted upstream.
|
||||
*/
|
||||
+
|
||||
virtual absl::optional<uint32_t> attemptCount() const PURE;
|
||||
+
|
||||
+#ifdef ALIMESH
|
||||
+ /**
|
||||
+ * @param key the filter state key set by wasm filter.
|
||||
+ * @param value the filter state value set by wasm filter.
|
||||
+ */
|
||||
+ virtual void setCustomSpanTag(const std::string& key, const std::string& value) PURE;
|
||||
+
|
||||
+ /**
|
||||
+ * @return the key-value map of filter states set by wasm filter.
|
||||
+ */
|
||||
+ virtual const std::unordered_map<std::string, std::string>& getCustomSpanTagMap() const PURE;
|
||||
+#endif
|
||||
};
|
||||
|
||||
} // namespace StreamInfo
|
||||
diff --git a/source/common/stream_info/stream_info_impl.h b/source/common/stream_info/stream_info_impl.h
|
||||
index 6ce2afe773..d5e7a80b37 100644
|
||||
--- a/source/common/stream_info/stream_info_impl.h
|
||||
+++ b/source/common/stream_info/stream_info_impl.h
|
||||
@@ -291,6 +291,20 @@ struct StreamInfoImpl : public StreamInfo {
|
||||
|
||||
absl::optional<uint32_t> attemptCount() const override { return attempt_count_; }
|
||||
|
||||
+#ifdef ALIMESH
|
||||
+ void setCustomSpanTag(const std::string& key, const std::string& value) override {
|
||||
+ auto it = custom_span_tags_.find(key);
|
||||
+ if (it != custom_span_tags_.end()) {
|
||||
+ it->second = value;
|
||||
+ } else {
|
||||
+ custom_span_tags_.emplace(key, value);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ const std::unordered_map<std::string, std::string>& getCustomSpanTagMap() const override {
|
||||
+ return custom_span_tags_;
|
||||
+ }
|
||||
+#endif
|
||||
TimeSource& time_source_;
|
||||
const SystemTime start_time_;
|
||||
const MonotonicTime start_time_monotonic_;
|
||||
@@ -350,6 +364,9 @@ private:
|
||||
absl::optional<Upstream::ClusterInfoConstSharedPtr> upstream_cluster_info_;
|
||||
std::string filter_chain_name_;
|
||||
Tracing::Reason trace_reason_;
|
||||
+#ifdef ALIMESH
|
||||
+ std::unordered_map<std::string, std::string> custom_span_tags_;
|
||||
+#endif
|
||||
};
|
||||
|
||||
} // namespace StreamInfo
|
||||
diff --git a/source/common/tracing/http_tracer_impl.cc b/source/common/tracing/http_tracer_impl.cc
|
||||
index e55cf00e0a..f94e9101d7 100644
|
||||
--- a/source/common/tracing/http_tracer_impl.cc
|
||||
+++ b/source/common/tracing/http_tracer_impl.cc
|
||||
@@ -214,6 +214,14 @@ void HttpTracerUtility::setCommonTags(Span& span, const Http::ResponseHeaderMap*
|
||||
|
||||
span.setTag(Tracing::Tags::get().Component, Tracing::Tags::get().Proxy);
|
||||
|
||||
+#ifdef ALIMESH
|
||||
+ // Wasm filter state
|
||||
+ const auto& custom_span_tags = stream_info.getCustomSpanTagMap();
|
||||
+ for (const auto& it : custom_span_tags) {
|
||||
+ span.setTag(it.first, it.second);
|
||||
+ }
|
||||
+#endif
|
||||
+
|
||||
if (nullptr != stream_info.upstreamHost()) {
|
||||
span.setTag(Tracing::Tags::get().UpstreamCluster, stream_info.upstreamHost()->cluster().name());
|
||||
span.setTag(Tracing::Tags::get().UpstreamClusterName,
|
||||
diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc
|
||||
index 410baa856f..b11ecf1cd6 100644
|
||||
--- a/source/extensions/common/wasm/context.cc
|
||||
+++ b/source/extensions/common/wasm/context.cc
|
||||
@@ -60,6 +60,7 @@ namespace {
|
||||
constexpr absl::string_view CelStateKeyPrefix = "wasm.";
|
||||
|
||||
#if defined(ALIMESH)
|
||||
+constexpr absl::string_view CustomeTraceSpanTagPrefix = "trace_span_tag.";
|
||||
constexpr std::string_view ClearRouteCacheKey = "clear_route_cache";
|
||||
constexpr std::string_view DisableClearRouteCache = "off";
|
||||
constexpr std::string_view SetDecoderBufferLimit = "set_decoder_buffer_limit";
|
||||
@@ -1271,6 +1272,13 @@ WasmResult Context::setProperty(std::string_view path, std::string_view value) {
|
||||
if (!stream_info) {
|
||||
return WasmResult::NotFound;
|
||||
}
|
||||
+#ifdef ALIMESH
|
||||
+ if (absl::StartsWith(absl::string_view{path.data(), path.size()}, CustomeTraceSpanTagPrefix)) {
|
||||
+ stream_info->setCustomSpanTag(std::string(path.substr(CustomeTraceSpanTagPrefix.size())),
|
||||
+ std::string(value));
|
||||
+ return WasmResult::Ok;
|
||||
+ }
|
||||
+#endif
|
||||
std::string key;
|
||||
absl::StrAppend(&key, CelStateKeyPrefix, toAbslStringView(path));
|
||||
CelState* state;
|
||||
341
get_helm.sh
Executable file
341
get_helm.sh
Executable file
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright The Helm Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# The install script is based off of the MIT-licensed script from glide,
|
||||
# the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get
|
||||
|
||||
: ${BINARY_NAME:="helm"}
|
||||
: ${USE_SUDO:="true"}
|
||||
: ${DEBUG:="false"}
|
||||
: ${VERIFY_CHECKSUM:="true"}
|
||||
: ${VERIFY_SIGNATURES:="false"}
|
||||
: ${HELM_INSTALL_DIR:="/usr/local/bin"}
|
||||
: ${GPG_PUBRING:="pubring.kbx"}
|
||||
|
||||
HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)"
|
||||
HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)"
|
||||
HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)"
|
||||
HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)"
|
||||
HAS_GIT="$(type "git" &> /dev/null && echo true || echo false)"
|
||||
|
||||
# initArch discovers the architecture for this system.
|
||||
initArch() {
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
armv5*) ARCH="armv5";;
|
||||
armv6*) ARCH="armv6";;
|
||||
armv7*) ARCH="arm";;
|
||||
aarch64) ARCH="arm64";;
|
||||
x86) ARCH="386";;
|
||||
x86_64) ARCH="amd64";;
|
||||
i686) ARCH="386";;
|
||||
i386) ARCH="386";;
|
||||
esac
|
||||
}
|
||||
|
||||
# initOS discovers the operating system for this system.
|
||||
initOS() {
|
||||
OS=$(echo `uname`|tr '[:upper:]' '[:lower:]')
|
||||
|
||||
case "$OS" in
|
||||
# Minimalist GNU for Windows
|
||||
mingw*|cygwin*) OS='windows';;
|
||||
esac
|
||||
}
|
||||
|
||||
# runs the given command as root (detects if we are root already)
|
||||
runAsRoot() {
|
||||
if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then
|
||||
sudo "${@}"
|
||||
else
|
||||
"${@}"
|
||||
fi
|
||||
}
|
||||
|
||||
# verifySupported checks that the os/arch combination is supported for
|
||||
# binary builds, as well whether or not necessary tools are present.
|
||||
verifySupported() {
|
||||
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
|
||||
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
|
||||
echo "No prebuilt binary for ${OS}-${ARCH}."
|
||||
echo "To build from source, go to https://github.com/helm/helm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${HAS_CURL}" != "true" ] && [ "${HAS_WGET}" != "true" ]; then
|
||||
echo "Either curl or wget is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${VERIFY_CHECKSUM}" == "true" ] && [ "${HAS_OPENSSL}" != "true" ]; then
|
||||
echo "In order to verify checksum, openssl must first be installed."
|
||||
echo "Please install openssl or set VERIFY_CHECKSUM=false in your environment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
|
||||
if [ "${HAS_GPG}" != "true" ]; then
|
||||
echo "In order to verify signatures, gpg must first be installed."
|
||||
echo "Please install gpg or set VERIFY_SIGNATURES=false in your environment."
|
||||
exit 1
|
||||
fi
|
||||
if [ "${OS}" != "linux" ]; then
|
||||
echo "Signature verification is currently only supported on Linux."
|
||||
echo "Please set VERIFY_SIGNATURES=false or verify the signatures manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${HAS_GIT}" != "true" ]; then
|
||||
echo "[WARNING] Could not find git. It is required for plugin installation."
|
||||
fi
|
||||
}
|
||||
|
||||
# checkDesiredVersion checks if the desired version is available.
|
||||
checkDesiredVersion() {
|
||||
if [ "x$DESIRED_VERSION" == "x" ]; then
|
||||
# Get tag from release URL
|
||||
local latest_release_url="https://get.helm.sh/helm-latest-version"
|
||||
local latest_release_response=""
|
||||
if [ "${HAS_CURL}" == "true" ]; then
|
||||
latest_release_response=$( curl -L --silent --show-error --fail "$latest_release_url" 2>&1 || true )
|
||||
elif [ "${HAS_WGET}" == "true" ]; then
|
||||
latest_release_response=$( wget "$latest_release_url" -q -O - 2>&1 || true )
|
||||
fi
|
||||
TAG=$( echo "$latest_release_response" | grep '^v[0-9]' )
|
||||
if [ "x$TAG" == "x" ]; then
|
||||
printf "Could not retrieve the latest release tag information from %s: %s\n" "${latest_release_url}" "${latest_release_response}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
TAG=$DESIRED_VERSION
|
||||
fi
|
||||
}
|
||||
|
||||
# checkHelmInstalledVersion checks which version of helm is installed and
|
||||
# if it needs to be changed.
|
||||
checkHelmInstalledVersion() {
|
||||
if [[ -f "${HELM_INSTALL_DIR}/${BINARY_NAME}" ]]; then
|
||||
local version=$("${HELM_INSTALL_DIR}/${BINARY_NAME}" version --template="{{ .Version }}")
|
||||
if [[ "$version" == "$TAG" ]]; then
|
||||
echo "Helm ${version} is already ${DESIRED_VERSION:-latest}"
|
||||
return 0
|
||||
else
|
||||
echo "Helm ${TAG} is available. Changing from version ${version}."
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# downloadFile downloads the latest binary package and also the checksum
|
||||
# for that binary.
|
||||
downloadFile() {
|
||||
HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz"
|
||||
DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST"
|
||||
CHECKSUM_URL="$DOWNLOAD_URL.sha256"
|
||||
HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)"
|
||||
HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST"
|
||||
HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256"
|
||||
echo "Downloading $DOWNLOAD_URL"
|
||||
if [ "${HAS_CURL}" == "true" ]; then
|
||||
curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE"
|
||||
curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE"
|
||||
elif [ "${HAS_WGET}" == "true" ]; then
|
||||
wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL"
|
||||
wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL"
|
||||
fi
|
||||
}
|
||||
|
||||
# verifyFile verifies the SHA256 checksum of the binary package
|
||||
# and the GPG signatures for both the package and checksum file
|
||||
# (depending on settings in environment).
|
||||
verifyFile() {
|
||||
if [ "${VERIFY_CHECKSUM}" == "true" ]; then
|
||||
verifyChecksum
|
||||
fi
|
||||
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
|
||||
verifySignatures
|
||||
fi
|
||||
}
|
||||
|
||||
# installFile installs the Helm binary.
|
||||
installFile() {
|
||||
HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME"
|
||||
mkdir -p "$HELM_TMP"
|
||||
tar xf "$HELM_TMP_FILE" -C "$HELM_TMP"
|
||||
HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm"
|
||||
echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}"
|
||||
runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME"
|
||||
echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME"
|
||||
}
|
||||
|
||||
# verifyChecksum verifies the SHA256 checksum of the binary package.
|
||||
verifyChecksum() {
|
||||
printf "Verifying checksum... "
|
||||
local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}')
|
||||
local expected_sum=$(cat ${HELM_SUM_FILE})
|
||||
if [ "$sum" != "$expected_sum" ]; then
|
||||
echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
echo "Done."
|
||||
}
|
||||
|
||||
# verifySignatures obtains the latest KEYS file from GitHub main branch
|
||||
# as well as the signature .asc files from the specific GitHub release,
|
||||
# then verifies that the release artifacts were signed by a maintainer's key.
|
||||
verifySignatures() {
|
||||
printf "Verifying signatures... "
|
||||
local keys_filename="KEYS"
|
||||
local github_keys_url="https://raw.githubusercontent.com/helm/helm/main/${keys_filename}"
|
||||
if [ "${HAS_CURL}" == "true" ]; then
|
||||
curl -SsL "${github_keys_url}" -o "${HELM_TMP_ROOT}/${keys_filename}"
|
||||
elif [ "${HAS_WGET}" == "true" ]; then
|
||||
wget -q -O "${HELM_TMP_ROOT}/${keys_filename}" "${github_keys_url}"
|
||||
fi
|
||||
local gpg_keyring="${HELM_TMP_ROOT}/keyring.gpg"
|
||||
local gpg_homedir="${HELM_TMP_ROOT}/gnupg"
|
||||
mkdir -p -m 0700 "${gpg_homedir}"
|
||||
local gpg_stderr_device="/dev/null"
|
||||
if [ "${DEBUG}" == "true" ]; then
|
||||
gpg_stderr_device="/dev/stderr"
|
||||
fi
|
||||
gpg --batch --quiet --homedir="${gpg_homedir}" --import "${HELM_TMP_ROOT}/${keys_filename}" 2> "${gpg_stderr_device}"
|
||||
gpg --batch --no-default-keyring --keyring "${gpg_homedir}/${GPG_PUBRING}" --export > "${gpg_keyring}"
|
||||
local github_release_url="https://github.com/helm/helm/releases/download/${TAG}"
|
||||
if [ "${HAS_CURL}" == "true" ]; then
|
||||
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
|
||||
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
|
||||
elif [ "${HAS_WGET}" == "true" ]; then
|
||||
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
|
||||
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
|
||||
fi
|
||||
local error_text="If you think this might be a potential security issue,"
|
||||
error_text="${error_text}\nplease see here: https://github.com/helm/community/blob/master/SECURITY.md"
|
||||
local num_goodlines_sha=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
|
||||
if [[ ${num_goodlines_sha} -lt 2 ]]; then
|
||||
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256!"
|
||||
echo -e "${error_text}"
|
||||
exit 1
|
||||
fi
|
||||
local num_goodlines_tar=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
|
||||
if [[ ${num_goodlines_tar} -lt 2 ]]; then
|
||||
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz!"
|
||||
echo -e "${error_text}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Done."
|
||||
}
|
||||
|
||||
# fail_trap is executed if an error occurs.
|
||||
fail_trap() {
|
||||
result=$?
|
||||
if [ "$result" != "0" ]; then
|
||||
if [[ -n "$INPUT_ARGUMENTS" ]]; then
|
||||
echo "Failed to install $BINARY_NAME with the arguments provided: $INPUT_ARGUMENTS"
|
||||
help
|
||||
else
|
||||
echo "Failed to install $BINARY_NAME"
|
||||
fi
|
||||
echo -e "\tFor support, go to https://github.com/helm/helm."
|
||||
fi
|
||||
cleanup
|
||||
exit $result
|
||||
}
|
||||
|
||||
# testVersion tests the installed client to make sure it is working.
|
||||
testVersion() {
|
||||
set +e
|
||||
HELM="$(command -v $BINARY_NAME)"
|
||||
if [ "$?" = "1" ]; then
|
||||
echo "$BINARY_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?'
|
||||
exit 1
|
||||
fi
|
||||
set -e
|
||||
}
|
||||
|
||||
# help provides possible cli installation arguments
|
||||
help () {
|
||||
echo "Accepted cli arguments are:"
|
||||
echo -e "\t[--help|-h ] ->> prints this help"
|
||||
echo -e "\t[--version|-v <desired_version>] . When not defined it fetches the latest release from GitHub"
|
||||
echo -e "\te.g. --version v3.0.0 or -v canary"
|
||||
echo -e "\t[--no-sudo] ->> install without sudo"
|
||||
}
|
||||
|
||||
# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977
|
||||
cleanup() {
|
||||
if [[ -d "${HELM_TMP_ROOT:-}" ]]; then
|
||||
rm -rf "$HELM_TMP_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
# Execution
|
||||
|
||||
#Stop execution on any error
|
||||
trap "fail_trap" EXIT
|
||||
set -e
|
||||
|
||||
# Set debug if desired
|
||||
if [ "${DEBUG}" == "true" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
# Parsing input arguments (if any)
|
||||
export INPUT_ARGUMENTS="${@}"
|
||||
set -u
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
'--version'|-v)
|
||||
shift
|
||||
if [[ $# -ne 0 ]]; then
|
||||
export DESIRED_VERSION="${1}"
|
||||
if [[ "$1" != "v"* ]]; then
|
||||
echo "Expected version arg ('${DESIRED_VERSION}') to begin with 'v', fixing..."
|
||||
export DESIRED_VERSION="v${1}"
|
||||
fi
|
||||
else
|
||||
echo -e "Please provide the desired version. e.g. --version v3.0.0 or -v canary"
|
||||
exit 0
|
||||
fi
|
||||
;;
|
||||
'--no-sudo')
|
||||
USE_SUDO="false"
|
||||
;;
|
||||
'--help'|-h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
*) exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
set +u
|
||||
|
||||
initArch
|
||||
initOS
|
||||
verifySupported
|
||||
checkDesiredVersion
|
||||
if ! checkHelmInstalledVersion; then
|
||||
downloadFile
|
||||
verifyFile
|
||||
installFile
|
||||
fi
|
||||
testVersion
|
||||
cleanup
|
||||
4
go.mod
4
go.mod
@@ -255,7 +255,6 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
@@ -304,7 +303,7 @@ replace istio.io/client-go => ./external/client-go
|
||||
|
||||
replace istio.io/istio => ./external/istio
|
||||
|
||||
replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0.1
|
||||
replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0.2
|
||||
|
||||
require (
|
||||
github.com/caddyserver/certmagic v0.20.0
|
||||
@@ -313,6 +312,7 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/net v0.17.0
|
||||
helm.sh/helm/v3 v3.7.1
|
||||
k8s.io/apiextensions-apiserver v0.25.4
|
||||
|
||||
4
go.sum
4
go.sum
@@ -61,8 +61,8 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/2456868764/certmagic v1.0.1 h1:dRzow2Npe9llFTBhNVl0fVe8Yi/Q14ygNonlaZUyDZQ=
|
||||
github.com/2456868764/certmagic v1.0.1/go.mod h1:LOn81EQYMPajdew6Ln6SVdHPxPqPv6jwsUg92kiNlcQ=
|
||||
github.com/2456868764/certmagic v1.0.2 h1:xYoN4z6seONwT85llWXZcASvQME8TOSiSWQvLJsGGsE=
|
||||
github.com/2456868764/certmagic v1.0.2/go.mod h1:LOn81EQYMPajdew6Ln6SVdHPxPqPv6jwsUg92kiNlcQ=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210929163055-e81b3f25be97/go.mod h1:WpB7kf89yJUETZxQnP1kgYPNwlT2jjdDYUCoxVggM3g=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.4.1
|
||||
appVersion: 1.4.2
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -10,4 +10,4 @@ name: higress-core
|
||||
sources:
|
||||
- http://github.com/alibaba/higress
|
||||
type: application
|
||||
version: 1.4.1
|
||||
version: 1.4.2
|
||||
|
||||
@@ -97,7 +97,7 @@ higress: {{ include "controller.name" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "skywalking.enabled" -}}
|
||||
{{- if and .Values.skywalking.enabled .Values.skywalking.service.address }}
|
||||
{{- if and (hasKey .Values "tracing") .Values.tracing.enable (hasKey .Values.tracing "skywalking") .Values.tracing.skywalking.service }}
|
||||
true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -46,10 +46,6 @@
|
||||
address: {{ .Values.global.tracer.lightstep.address }}
|
||||
# Access Token used to communicate with the Satellite pool
|
||||
accessToken: {{ .Values.global.tracer.lightstep.accessToken }}
|
||||
{{- else if eq .Values.global.proxy.tracer "zipkin" }}
|
||||
zipkin:
|
||||
# Address of the Zipkin collector
|
||||
address: {{ .Values.global.tracer.zipkin.address | default (print "zipkin." .Release.Namespace ":9411") }}
|
||||
{{- else if eq .Values.global.proxy.tracer "datadog" }}
|
||||
datadog:
|
||||
# Address of the Datadog Agent
|
||||
@@ -88,7 +84,7 @@
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
discoveryAddress: {{ printf "istiod.%s.svc" .Values.global.istioNamespace }}:15012
|
||||
{{- else }}
|
||||
discoveryAddress: higress-controller.{{.Release.Namespace}}.svc:15012
|
||||
discoveryAddress: {{ include "controller.name" . }}.{{.Release.Namespace}}.svc:15012
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
proxyStatsMatcher:
|
||||
@@ -109,7 +105,17 @@ metadata:
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
data:
|
||||
|
||||
higress: |-
|
||||
{{- $existingConfig := lookup "v1" "ConfigMap" .Release.Namespace "higress-config" }}
|
||||
{{- $existingData := dict }}
|
||||
{{- if $existingConfig }}
|
||||
{{- $existingData = index $existingConfig.data "higress" | default "{}" | fromYaml }}
|
||||
{{- end }}
|
||||
{{- $newData := dict }}
|
||||
{{- if and (hasKey .Values "tracing") .Values.tracing.enable }}
|
||||
{{- $_ := set $newData "tracing" .Values.tracing }}
|
||||
{{- end }}
|
||||
{{- toYaml (merge $existingData $newData) | nindent 4 }}
|
||||
# Configuration file for the mesh networks to be used by the Split Horizon EDS.
|
||||
meshNetworks: |-
|
||||
{{- if .Values.global.meshNetworks }}
|
||||
@@ -170,8 +176,8 @@ data:
|
||||
"endpoint": {
|
||||
"address": {
|
||||
"socket_address": {
|
||||
"address": "{{ .Values.skywalking.service.address }}",
|
||||
"port_value": "{{ .Values.skywalking.service.port }}"
|
||||
"address": "{{ .Values.tracing.skywalking.service }}",
|
||||
"port_value": "{{ .Values.tracing.skywalking.port }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ rules:
|
||||
# ingress controller
|
||||
- apiGroups: ["extensions", "networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "delete", "patch"]
|
||||
- apiGroups: ["extensions", "networking.k8s.io"]
|
||||
resources: ["ingresses/status"]
|
||||
verbs: ["*"]
|
||||
@@ -36,7 +36,7 @@ rules:
|
||||
# Needed for multicluster secret reading, possibly ingress certs in the future
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
|
||||
- apiGroups: ["networking.higress.io"]
|
||||
resources: ["mcpbridges"]
|
||||
@@ -61,12 +61,12 @@ rules:
|
||||
|
||||
# discovery and routing
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "nodes", "services", "namespaces", "endpoints"]
|
||||
resources: ["pods", "nodes", "services", "namespaces", "endpoints", "deployments"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["discovery.k8s.io"]
|
||||
resources: ["endpointslices"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
|
||||
# Istiod and bootstrap.
|
||||
- apiGroups: ["certificates.k8s.io"]
|
||||
resources:
|
||||
@@ -100,7 +100,7 @@ rules:
|
||||
- apiGroups: ["multicluster.x-k8s.io"]
|
||||
resources: ["serviceimports"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
|
||||
|
||||
# sidecar injection controller
|
||||
- apiGroups: ["admissionregistration.k8s.io"]
|
||||
resources: ["mutatingwebhookconfigurations"]
|
||||
|
||||
@@ -26,9 +26,70 @@ spec:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "controller.serviceAccountName" . }}
|
||||
{{- if .Values.global.priorityClassName }}
|
||||
priorityClassName: "{{ .Values.global.priorityClassName }}"
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.controller.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.controller.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.controller.hub | default .Values.global.hub }}/{{ .Values.controller.image | default "higress" }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
|
||||
args:
|
||||
- "serve"
|
||||
- --gatewaySelectorKey=higress
|
||||
- --gatewaySelectorValue={{ .Release.Namespace }}-{{ include "gateway.name" . }}
|
||||
- --gatewayHttpPort={{ .Values.gateway.httpPort }}
|
||||
- --gatewayHttpsPort={{ .Values.gateway.httpsPort }}
|
||||
{{- if not .Values.global.enableStatus }}
|
||||
- --enableStatus={{ .Values.global.enableStatus }}
|
||||
{{- end }}
|
||||
- --ingressClass={{ .Values.global.ingressClass }}
|
||||
{{- if .Values.global.watchNamespace }}
|
||||
- --watchNamespace={{ .Values.global.watchNamespace }}
|
||||
{{- end }}
|
||||
- --enableAutomaticHttps={{ .Values.controller.automaticHttps.enabled }}
|
||||
- --automaticHttpsEmail={{ .Values.controller.automaticHttps.email }}
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
- name: SERVICE_ACCOUNT
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: spec.serviceAccountName
|
||||
- name: DOMAIN_SUFFIX
|
||||
value: {{ .Values.global.proxy.clusterDomain }}
|
||||
{{- if .Values.controller.env }}
|
||||
{{- range $key, $val := .Values.controller.env }}
|
||||
- name: {{ $key }}
|
||||
value: "{{ $val }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
{{- range $idx, $port := .Values.controller.ports }}
|
||||
- name: {{ $port.name }}
|
||||
containerPort: {{ $port.port }}
|
||||
protocol: {{ $port.protocol }}
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.controller.probe | nindent 12 }}
|
||||
{{- if not (or .Values.global.local .Values.global.kind) }}
|
||||
resources:
|
||||
{{- toYaml .Values.controller.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: log
|
||||
mountPath: /var/log
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: discovery
|
||||
image: "{{ .Values.pilot.hub | default .Values.global.hub }}/{{ .Values.pilot.image | default "pilot" }}:{{ .Values.pilot.tag | default .Chart.AppVersion }}"
|
||||
@@ -191,64 +252,6 @@ spec:
|
||||
mountPath: /cacerts
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.controller.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.controller.hub | default .Values.global.hub }}/{{ .Values.controller.image | default "higress" }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
|
||||
args:
|
||||
- "serve"
|
||||
- --gatewaySelectorKey=higress
|
||||
- --gatewaySelectorValue={{ .Release.Namespace }}-{{ include "gateway.name" . }}
|
||||
- --gatewayHttpPort={{ .Values.gateway.httpPort }}
|
||||
- --gatewayHttpsPort={{ .Values.gateway.httpsPort }}
|
||||
{{- if not .Values.global.enableStatus }}
|
||||
- --enableStatus={{ .Values.global.enableStatus }}
|
||||
{{- end }}
|
||||
- --ingressClass={{ .Values.global.ingressClass }}
|
||||
{{- if .Values.global.watchNamespace }}
|
||||
- --watchNamespace={{ .Values.global.watchNamespace }}
|
||||
{{- end }}
|
||||
- --enableAutomaticHttps={{ .Values.controller.automaticHttps.enabled }}
|
||||
- --automaticHttpsEmail={{ .Values.controller.automaticHttps.email }}
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
- name: SERVICE_ACCOUNT
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: spec.serviceAccountName
|
||||
- name: DOMAIN_SUFFIX
|
||||
value: {{ .Values.global.proxy.clusterDomain }}
|
||||
{{- if .Values.controller.env }}
|
||||
{{- range $key, $val := .Values.controller.env }}
|
||||
- name: {{ $key }}
|
||||
value: "{{ $val }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
{{- range $idx, $port := .Values.controller.ports }}
|
||||
- name: {{ $port.name }}
|
||||
containerPort: {{ $port.port }}
|
||||
protocol: {{ $port.protocol }}
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.controller.probe | nindent 12 }}
|
||||
{{- if not (or .Values.global.local .Values.global.kind) }}
|
||||
resources:
|
||||
{{- toYaml .Values.controller.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: log
|
||||
mountPath: /var/log
|
||||
{{- with .Values.controller.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
||||
332
helm/core/templates/daemonset.yaml
Normal file
332
helm/core/templates/daemonset.yaml
Normal file
@@ -0,0 +1,332 @@
|
||||
{{- if eq .Values.gateway.kind "DaemonSet" -}}
|
||||
{{- $o11y := .Values.global.o11y }}
|
||||
{{- $unprivilegedPortSupported := true }}
|
||||
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
|
||||
{{- $kernelVersion := $node.status.nodeInfo.kernelVersion }}
|
||||
{{- if $kernelVersion }}
|
||||
{{- $kernelVersion = regexFind "^(\\d+\\.\\d+\\.\\d+)" $kernelVersion }}
|
||||
{{- if and $kernelVersion (semverCompare "<4.11.0" $kernelVersion) }}
|
||||
{{- $unprivilegedPortSupported = false }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: {{ include "gateway.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4}}
|
||||
annotations:
|
||||
{{- .Values.gateway.annotations | toYaml | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gateway.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
"enableHigressIstio": "true"
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.podAnnotations }}
|
||||
{{- toYaml .Values.gateway.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
sidecar.istio.io/inject: "false"
|
||||
{{- with .Values.gateway.revision }}
|
||||
istio.io/rev: {{ . }}
|
||||
{{- end }}
|
||||
{{- include "gateway.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.gateway.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "gateway.serviceAccountName" . }}
|
||||
{{- if .Values.global.priorityClassName }}
|
||||
priorityClassName: "{{ .Values.global.priorityClassName }}"
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- if .Values.gateway.securityContext }}
|
||||
{{- toYaml .Values.gateway.securityContext | nindent 8 }}
|
||||
{{- else if and $unprivilegedPortSupported (and (not .Values.gateway.hostNetwork) (semverCompare ">=1.22-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
# Safe since 1.22: https://github.com/kubernetes/kubernetes/pull/103326
|
||||
sysctls:
|
||||
- name: net.ipv4.ip_unprivileged_port_start
|
||||
value: "0"
|
||||
{{- end }}
|
||||
containers:
|
||||
{{- if $o11y.enabled }}
|
||||
{{- $config := $o11y.promtail }}
|
||||
- name: promtail
|
||||
image: {{ $config.image.repository }}:{{ $config.image.tag }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -config.file=/etc/promtail/promtail.yaml
|
||||
env:
|
||||
- name: 'HOSTNAME'
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: 'spec.nodeName'
|
||||
ports:
|
||||
- containerPort: {{ $config.port }}
|
||||
name: http-metrics
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: {{ $config.port }}
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
volumeMounts:
|
||||
- name: promtail-config
|
||||
mountPath: "/etc/promtail"
|
||||
- name: log
|
||||
mountPath: /var/log/proxy
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
{{- end }}
|
||||
- name: higress-gateway
|
||||
image: "{{ .Values.gateway.hub | default .Values.global.hub }}/{{ .Values.gateway.image | default "gateway" }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
|
||||
args:
|
||||
- proxy
|
||||
- router
|
||||
- --domain
|
||||
- $(POD_NAMESPACE).svc.cluster.local
|
||||
- --proxyLogLevel=warning
|
||||
- --proxyComponentLogLevel=misc:error
|
||||
- --log_output_level=all:info
|
||||
- --serviceCluster=higress-gateway
|
||||
securityContext:
|
||||
{{- if .Values.gateway.containerSecurityContext }}
|
||||
{{- toYaml .Values.gateway.containerSecurityContext | nindent 12 }}
|
||||
{{- else if and $unprivilegedPortSupported (and (not .Values.gateway.hostNetwork) (semverCompare ">=1.22-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
# Safe since 1.22: https://github.com/kubernetes/kubernetes/pull/103326
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
# When enabling lite metrics, the configuration template files need to be replaced.
|
||||
{{- if not .Values.global.liteMetrics }}
|
||||
readOnlyRootFilesystem: true
|
||||
{{- end }}
|
||||
runAsUser: 1337
|
||||
runAsGroup: 1337
|
||||
runAsNonRoot: true
|
||||
{{- else }}
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
runAsUser: 0
|
||||
runAsGroup: 1337
|
||||
runAsNonRoot: false
|
||||
allowPrivilegeEscalation: true
|
||||
{{- end }}
|
||||
env:
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: spec.nodeName
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
- name: INSTANCE_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: status.podIP
|
||||
- name: HOST_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: status.hostIP
|
||||
- name: SERVICE_ACCOUNT
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.serviceAccountName
|
||||
- name: PILOT_XDS_SEND_TIMEOUT
|
||||
value: 60s
|
||||
- name: PROXY_XDS_VIA_AGENT
|
||||
value: "true"
|
||||
- name: ENABLE_INGRESS_GATEWAY_SDS
|
||||
value: "false"
|
||||
- name: JWT_POLICY
|
||||
value: {{ include "controller.jwtPolicy" . }}
|
||||
- name: ISTIO_META_HTTP10
|
||||
value: "1"
|
||||
- name: ISTIO_META_CLUSTER_ID
|
||||
value: "{{ $.Values.clusterName | default `Kubernetes` }}"
|
||||
- name: INSTANCE_NAME
|
||||
value: "higress-gateway"
|
||||
{{- if .Values.global.liteMetrics }}
|
||||
- name: LITE_METRICS
|
||||
value: "on"
|
||||
{{- end }}
|
||||
{{- if include "skywalking.enabled" . }}
|
||||
- name: ISTIO_BOOTSTRAP_OVERRIDE
|
||||
value: /etc/istio/custom-bootstrap/custom_bootstrap.json
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.networkGateway }}
|
||||
- name: ISTIO_META_REQUESTED_NETWORK_VIEW
|
||||
value: "{{.}}"
|
||||
{{- end }}
|
||||
{{- range $key, $val := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 15090
|
||||
protocol: TCP
|
||||
name: http-envoy-prom
|
||||
{{- if or .Values.global.local .Values.global.kind }}
|
||||
- containerPort: {{ .Values.gateway.httpPort }}
|
||||
hostPort: {{ .Values.gateway.httpPort }}
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: {{ .Values.gateway.httpsPort }}
|
||||
hostPort: {{ .Values.gateway.httpsPort }}
|
||||
name: https
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
failureThreshold: {{ .Values.gateway.readinessFailureThreshold }}
|
||||
httpGet:
|
||||
path: /healthz/ready
|
||||
port: 15021
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: {{ .Values.gateway.readinessInitialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.gateway.readinessPeriodSeconds }}
|
||||
successThreshold: {{ .Values.gateway.readinessSuccessThreshold }}
|
||||
timeoutSeconds: {{ .Values.gateway.readinessTimeoutSeconds }}
|
||||
{{- if not (or .Values.global.local .Values.global.kind) }}
|
||||
resources:
|
||||
{{- toYaml .Values.gateway.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
{{- if eq (include "controller.jwtPolicy" .) "third-party-jwt" }}
|
||||
- name: istio-token
|
||||
mountPath: /var/run/secrets/tokens
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
- name: config
|
||||
mountPath: /etc/istio/config
|
||||
- name: istio-ca-root-cert
|
||||
mountPath: /var/run/secrets/istio
|
||||
- name: istio-data
|
||||
mountPath: /var/lib/istio/data
|
||||
- name: podinfo
|
||||
mountPath: /etc/istio/pod
|
||||
- name: proxy-socket
|
||||
mountPath: /etc/istio/proxy
|
||||
{{- if include "skywalking.enabled" . }}
|
||||
- mountPath: /etc/istio/custom-bootstrap
|
||||
name: custom-bootstrap-volume
|
||||
{{- end }}
|
||||
{{- if .Values.global.volumeWasmPlugins }}
|
||||
- mountPath: /opt/plugins
|
||||
name: local-wasmplugins-volume
|
||||
{{- end }}
|
||||
{{- if $o11y.enabled }}
|
||||
- mountPath: /var/log/proxy
|
||||
name: log
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.hostNetwork }}
|
||||
hostNetwork: {{ .Values.gateway.hostNetwork }}
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- if eq (include "controller.jwtPolicy" .) "third-party-jwt" }}
|
||||
- name: istio-token
|
||||
projected:
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
audience: istio-ca
|
||||
expirationSeconds: 43200
|
||||
path: istio-token
|
||||
{{- end }}
|
||||
- name: istio-ca-root-cert
|
||||
configMap:
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
name: istio-ca-root-cert
|
||||
{{- else }}
|
||||
name: higress-ca-root-cert
|
||||
{{- end }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: higress-config
|
||||
{{- if include "skywalking.enabled" . }}
|
||||
- configMap:
|
||||
defaultMode: 420
|
||||
name: higress-custom-bootstrap
|
||||
name: custom-bootstrap-volume
|
||||
{{- end }}
|
||||
- name: istio-data
|
||||
emptyDir: {}
|
||||
- name: proxy-socket
|
||||
emptyDir: {}
|
||||
{{- if $o11y.enabled }}
|
||||
- name: log
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: promtail-config
|
||||
configMap:
|
||||
name: higress-promtail
|
||||
{{- end }}
|
||||
- name: podinfo
|
||||
downwardAPI:
|
||||
defaultMode: 420
|
||||
items:
|
||||
- fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.labels
|
||||
path: labels
|
||||
- fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.annotations
|
||||
path: annotations
|
||||
- path: cpu-request
|
||||
resourceFieldRef:
|
||||
containerName: higress-gateway
|
||||
divisor: 1m
|
||||
resource: requests.cpu
|
||||
- path: cpu-limit
|
||||
resourceFieldRef:
|
||||
containerName: higress-gateway
|
||||
divisor: 1m
|
||||
resource: limits.cpu
|
||||
{{- if .Values.global.volumeWasmPlugins }}
|
||||
- name: local-wasmplugins-volume
|
||||
hostPath:
|
||||
path: /opt/plugins
|
||||
type: Directory
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if eq .Values.gateway.kind "Deployment" -}}
|
||||
{{- $o11y := .Values.global.o11y }}
|
||||
{{- $unprivilegedPortSupported := true }}
|
||||
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
|
||||
@@ -58,6 +59,9 @@ spec:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "gateway.serviceAccountName" . }}
|
||||
{{- if .Values.global.priorityClassName }}
|
||||
priorityClassName: "{{ .Values.global.priorityClassName }}"
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- if .Values.gateway.securityContext }}
|
||||
{{- toYaml .Values.gateway.securityContext | nindent 8 }}
|
||||
@@ -68,40 +72,6 @@ spec:
|
||||
value: "0"
|
||||
{{- end }}
|
||||
containers:
|
||||
{{- if $o11y.enabled }}
|
||||
{{- $config := $o11y.promtail }}
|
||||
- name: promtail
|
||||
image: {{ $config.image.repository }}:{{ $config.image.tag }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -config.file=/etc/promtail/promtail.yaml
|
||||
env:
|
||||
- name: 'HOSTNAME'
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: 'spec.nodeName'
|
||||
ports:
|
||||
- containerPort: {{ $config.port }}
|
||||
name: http-metrics
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: {{ $config.port }}
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
volumeMounts:
|
||||
- name: promtail-config
|
||||
mountPath: "/etc/promtail"
|
||||
- name: log
|
||||
mountPath: /var/log/proxy
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
{{- end }}
|
||||
- name: higress-gateway
|
||||
image: "{{ .Values.gateway.hub | default .Values.global.hub }}/{{ .Values.gateway.image | default "gateway" }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
|
||||
args:
|
||||
@@ -202,6 +172,9 @@ spec:
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 15020
|
||||
protocol: TCP
|
||||
name: istio-prom
|
||||
- containerPort: 15090
|
||||
protocol: TCP
|
||||
name: http-envoy-prom
|
||||
@@ -241,7 +214,7 @@ spec:
|
||||
mountPath: /var/run/secrets/istio
|
||||
- name: istio-data
|
||||
mountPath: /var/lib/istio/data
|
||||
- name: podinfo
|
||||
- name: podinfo
|
||||
mountPath: /etc/istio/pod
|
||||
- name: proxy-socket
|
||||
mountPath: /etc/istio/proxy
|
||||
@@ -257,6 +230,40 @@ spec:
|
||||
- mountPath: /var/log/proxy
|
||||
name: log
|
||||
{{- end }}
|
||||
{{- if $o11y.enabled }}
|
||||
{{- $config := $o11y.promtail }}
|
||||
- name: promtail
|
||||
image: {{ $config.image.repository }}:{{ $config.image.tag }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -config.file=/etc/promtail/promtail.yaml
|
||||
env:
|
||||
- name: 'HOSTNAME'
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: 'spec.nodeName'
|
||||
ports:
|
||||
- containerPort: {{ $config.port }}
|
||||
name: http-metrics
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: {{ $config.port }}
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
volumeMounts:
|
||||
- name: promtail-config
|
||||
mountPath: "/etc/promtail"
|
||||
- name: log
|
||||
mountPath: /var/log/proxy
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.hostNetwork }}
|
||||
hostNetwork: {{ .Values.gateway.hostNetwork }}
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
@@ -340,3 +347,4 @@ spec:
|
||||
path: /opt/plugins
|
||||
type: Directory
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
6
helm/core/templates/ingressclass.yaml
Normal file
6
helm/core/templates/ingressclass.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: {{ .Values.global.ingressClass }}
|
||||
spec:
|
||||
controller: higress.io/higress-controller
|
||||
@@ -15,6 +15,9 @@ spec:
|
||||
{{- with .Values.gateway.service.loadBalancerIP }}
|
||||
loadBalancerIP: "{{ . }}"
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.service.loadBalancerClass }}
|
||||
loadBalancerClass: "{{ . }}"
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.service.loadBalancerSourceRanges }}
|
||||
loadBalancerSourceRanges:
|
||||
{{ toYaml . | indent 4 }}
|
||||
|
||||
@@ -178,9 +178,9 @@ global:
|
||||
# Default port for Pilot agent health checks. A value of 0 will disable health checking.
|
||||
statusPort: 15020
|
||||
|
||||
# Specify which tracer to use. One of: zipkin, lightstep, datadog, stackdriver.
|
||||
# Specify which tracer to use. One of: lightstep, datadog, stackdriver.
|
||||
# If using stackdriver tracer outside GCP, set env GOOGLE_APPLICATION_CREDENTIALS to the GCP credential file.
|
||||
tracer: "zipkin"
|
||||
tracer: ""
|
||||
|
||||
# Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
|
||||
holdApplicationUntilProxyStarts: false
|
||||
@@ -330,12 +330,8 @@ global:
|
||||
maxNumberOfAnnotations: 200
|
||||
# The global default max number of attributes per span.
|
||||
maxNumberOfAttributes: 200
|
||||
zipkin:
|
||||
# Host:Port for reporting trace data in zipkin format. If not specified, will default to
|
||||
# zipkin service (port 9411) in the same namespace as the other istio components.
|
||||
address: ""
|
||||
|
||||
# Use the Mesh Control Protocol (MCP) for configuring Istiod. Requires an MCP source.
|
||||
|
||||
useMCP: false
|
||||
|
||||
# Observability (o11y) configurations
|
||||
@@ -343,7 +339,7 @@ global:
|
||||
enabled: false
|
||||
promtail:
|
||||
image:
|
||||
repository: grafana/promtail
|
||||
repository: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/promtail
|
||||
tag: 2.9.4
|
||||
port: 3101
|
||||
resources:
|
||||
@@ -396,6 +392,9 @@ gateway:
|
||||
replicas: 2
|
||||
image: gateway
|
||||
|
||||
# -- Use a `DaemonSet` or `Deployment`
|
||||
kind: Deployment
|
||||
|
||||
# The number of successive failed probes before indicating readiness failure.
|
||||
readinessFailureThreshold: 30
|
||||
|
||||
@@ -468,6 +467,7 @@ gateway:
|
||||
targetPort: 443
|
||||
annotations: {}
|
||||
loadBalancerIP: ""
|
||||
loadBalancerClass: ""
|
||||
loadBalancerSourceRanges: []
|
||||
externalTrafficPolicy: ""
|
||||
|
||||
@@ -664,9 +664,15 @@ pilot:
|
||||
podLabels: {}
|
||||
|
||||
|
||||
# Skywalking config settings
|
||||
skywalking:
|
||||
enabled: false
|
||||
service:
|
||||
address: ~
|
||||
port: 11800
|
||||
# Tracing config settings
|
||||
tracing:
|
||||
enable: false
|
||||
sampling: 100
|
||||
timeout: 500
|
||||
skywalking:
|
||||
# access_token: ""
|
||||
service: ""
|
||||
port: 11800
|
||||
# zipkin:
|
||||
# service: ""
|
||||
# port: 9411
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 1.4.1
|
||||
version: 1.4.2
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.4.1
|
||||
digest: sha256:de41b8f771e869aef9b83d2334fea5d34492a1c5df37e5aaff383189877cba23
|
||||
generated: "2024-06-19T17:10:02.426994+08:00"
|
||||
version: 1.4.2
|
||||
digest: sha256:31b557e55584e589b140ae9b89cfc8b99df91771c7d28465c3a2b06a4f35a192
|
||||
generated: "2024-07-26T13:53:23.225023+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.4.1
|
||||
appVersion: 1.4.2
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 1.4.1
|
||||
version: 1.4.2
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.4.1
|
||||
version: 1.4.2
|
||||
type: application
|
||||
version: 1.4.1
|
||||
version: 1.4.2
|
||||
|
||||
@@ -391,7 +391,7 @@ func (s *Server) initAutomaticHttps() error {
|
||||
ServerAddress: s.CertHttpAddress,
|
||||
Email: s.AutomaticHttpsEmail,
|
||||
}
|
||||
certServer, err := cert.NewServer(s.kubeClient.Kube(), certOption)
|
||||
certServer, err := cert.NewServer(s.kubeClient.Kube(), s.xdsServer, certOption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,10 +17,15 @@ package cert
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
@@ -28,6 +33,10 @@ const (
|
||||
EventCertObtained = "cert_obtained"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *certmagic.Config
|
||||
)
|
||||
|
||||
type CertMgr struct {
|
||||
cfg *certmagic.Config
|
||||
client kubernetes.Interface
|
||||
@@ -39,9 +48,10 @@ type CertMgr struct {
|
||||
ingressSolver acmez.Solver
|
||||
configMgr *ConfigMgr
|
||||
secretMgr *SecretMgr
|
||||
XDSUpdater model.XDSUpdater
|
||||
}
|
||||
|
||||
func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (*CertMgr, error) {
|
||||
func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config, XDSUpdater model.XDSUpdater, configMgr *ConfigMgr) (*CertMgr, error) {
|
||||
CertLog.Infof("certmgr init config: %+v", config)
|
||||
// Init certmagic config
|
||||
// First make a pointer to a Cache as we need to reference the same Cache in
|
||||
@@ -49,21 +59,29 @@ func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (
|
||||
var cache *certmagic.Cache
|
||||
var storage certmagic.Storage
|
||||
storage, _ = NewConfigmapStorage(opts.Namespace, clientSet)
|
||||
renewalWindowRatio := float64(config.RenewBeforeDays / RenewMaxDays)
|
||||
renewalWindowRatio := float64(config.RenewBeforeDays) / float64(RenewMaxDays)
|
||||
logger := zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
|
||||
os.Stderr,
|
||||
zap.DebugLevel,
|
||||
))
|
||||
magicConfig := certmagic.Config{
|
||||
RenewalWindowRatio: renewalWindowRatio,
|
||||
Storage: storage,
|
||||
Logger: logger,
|
||||
}
|
||||
cache = certmagic.NewCache(certmagic.CacheOptions{
|
||||
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||
// Here we use New to get a valid Config associated with the same cache.
|
||||
// The provided Config is used as a template and will be completed with
|
||||
// any defaults that are set in the Default config.
|
||||
return certmagic.New(cache, magicConfig), nil
|
||||
return cfg, nil
|
||||
},
|
||||
Logger: logger,
|
||||
})
|
||||
// init certmagic
|
||||
cfg := certmagic.New(cache, magicConfig)
|
||||
cfg = certmagic.New(cache, magicConfig)
|
||||
|
||||
// Init certmagic acme
|
||||
issuer := config.GetIssuer(IssuerTypeLetsencrypt)
|
||||
if issuer == nil {
|
||||
@@ -85,7 +103,6 @@ func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (
|
||||
// init issuers
|
||||
cfg.Issuers = []certmagic.Issuer{myACME}
|
||||
|
||||
configMgr, _ := NewConfigMgr(opts.Namespace, clientSet)
|
||||
secretMgr, _ := NewSecretMgr(opts.Namespace, clientSet)
|
||||
|
||||
certMgr := &CertMgr{
|
||||
@@ -97,6 +114,7 @@ func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config) (
|
||||
configMgr: configMgr,
|
||||
secretMgr: secretMgr,
|
||||
cache: cache,
|
||||
XDSUpdater: XDSUpdater,
|
||||
}
|
||||
certMgr.cfg.OnEvent = certMgr.OnEvent
|
||||
return certMgr, nil
|
||||
@@ -149,18 +167,31 @@ func (s *CertMgr) Reconcile(ctx context.Context, oldConfig *Config, newConfig *C
|
||||
// sync email
|
||||
s.myACME.Email = newIssuer.Email
|
||||
// sync RenewalWindowRatio
|
||||
s.cfg.RenewalWindowRatio = float64(newConfig.RenewBeforeDays / RenewMaxDays)
|
||||
renewalWindowRatio := float64(newConfig.RenewBeforeDays) / float64(RenewMaxDays)
|
||||
s.cfg.RenewalWindowRatio = renewalWindowRatio
|
||||
// start cache
|
||||
s.cache.Start()
|
||||
// sync domains
|
||||
s.manageSync(context.Background(), newDomains)
|
||||
s.configMgr.SetConfig(newConfig)
|
||||
CertLog.Infof("certMgr start to manageSync domains:+v%", newDomains)
|
||||
s.manageSync(context.Background(), newDomains)
|
||||
CertLog.Infof("certMgr manageSync domains done")
|
||||
} else {
|
||||
// stop cache maintainAssets
|
||||
s.cache.Stop()
|
||||
s.configMgr.SetConfig(newConfig)
|
||||
}
|
||||
|
||||
if oldConfig != nil && newConfig != nil {
|
||||
if oldConfig.FallbackForInvalidSecret != newConfig.FallbackForInvalidSecret || !reflect.DeepEqual(oldConfig.CredentialConfig, newConfig.CredentialConfig) {
|
||||
CertLog.Infof("ingress need to full push")
|
||||
s.XDSUpdater.ConfigUpdate(&model.PushRequest{
|
||||
Full: true,
|
||||
Reason: []model.TriggerReason{"higress-https-updated"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -86,22 +86,35 @@ func (c *Config) GetSecretNameByDomain(issuerName IssuerName, domain string) str
|
||||
return ""
|
||||
}
|
||||
|
||||
func ParseTLSSecret(tlsSecret string) (string, string) {
|
||||
secrets := strings.Split(tlsSecret, "/")
|
||||
switch len(secrets) {
|
||||
case 1:
|
||||
return "", tlsSecret
|
||||
case 2:
|
||||
return secrets[0], secrets[1]
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
// check acmeIssuer
|
||||
if len(c.ACMEIssuer) == 0 {
|
||||
return fmt.Errorf("acmeIssuer is empty")
|
||||
}
|
||||
for _, issuer := range c.ACMEIssuer {
|
||||
switch issuer.Name {
|
||||
case IssuerTypeLetsencrypt:
|
||||
if issuer.Email == "" {
|
||||
return fmt.Errorf("acmeIssuer %s email is empty", issuer.Name)
|
||||
if c.AutomaticHttps {
|
||||
if len(c.ACMEIssuer) == 0 {
|
||||
return fmt.Errorf("no acmeIssuer configuration found when automaticHttps is enable")
|
||||
}
|
||||
for _, issuer := range c.ACMEIssuer {
|
||||
switch issuer.Name {
|
||||
case IssuerTypeLetsencrypt:
|
||||
if issuer.Email == "" {
|
||||
return fmt.Errorf("acmeIssuer %s email is empty", issuer.Name)
|
||||
}
|
||||
if !ValidateEmail(issuer.Email) {
|
||||
return fmt.Errorf("acmeIssuer %s email %s is invalid", issuer.Name, issuer.Email)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("acmeIssuer name %s is not supported", issuer.Name)
|
||||
}
|
||||
if !ValidateEmail(issuer.Email) {
|
||||
return fmt.Errorf("acmeIssuer %s email %s is invalid", issuer.Name, issuer.Email)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("acmeIssuer name %s is not supported", issuer.Name)
|
||||
}
|
||||
}
|
||||
// check credentialConfig
|
||||
@@ -111,14 +124,20 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
if credential.TLSSecret == "" {
|
||||
return fmt.Errorf("credentialConfig tlsSecret is empty")
|
||||
} else {
|
||||
ns, secret := ParseTLSSecret(credential.TLSSecret)
|
||||
if ns == "" && secret == "" {
|
||||
return fmt.Errorf("credentialConfig tlsSecret %s is not supported", credential.TLSSecret)
|
||||
}
|
||||
}
|
||||
|
||||
if credential.TLSIssuer == IssuerTypeLetsencrypt {
|
||||
if len(credential.Domains) > 1 {
|
||||
return fmt.Errorf("credentialConfig tlsIssuer %s only support one domain", credential.TLSIssuer)
|
||||
}
|
||||
}
|
||||
if credential.TLSIssuer != IssuerTypeLetsencrypt && len(credential.TLSIssuer) > 0 {
|
||||
return fmt.Errorf("credential tls issuer %s is not support", credential.TLSIssuer)
|
||||
return fmt.Errorf("credential tls issuer %s is not supported", credential.TLSIssuer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,3 +120,36 @@ func TestMatchSecretNameByDomain(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTLSSecret(t *testing.T) {
|
||||
tests := []struct {
|
||||
tlsSecret string
|
||||
expectedNamespace string
|
||||
expectedSecretName string
|
||||
}{
|
||||
{
|
||||
tlsSecret: "example-com-tls",
|
||||
expectedNamespace: "",
|
||||
expectedSecretName: "example-com-tls",
|
||||
},
|
||||
|
||||
{
|
||||
tlsSecret: "kube-system/example-com-tls",
|
||||
expectedNamespace: "kube-system",
|
||||
expectedSecretName: "example-com-tls",
|
||||
},
|
||||
{
|
||||
tlsSecret: "kube-system/example-com/wildcard",
|
||||
expectedNamespace: "",
|
||||
expectedSecretName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.tlsSecret, func(t *testing.T) {
|
||||
resultNamespace, resultSecretName := ParseTLSSecret(tt.tlsSecret)
|
||||
assert.Equal(t, tt.expectedNamespace, resultNamespace)
|
||||
assert.Equal(t, tt.expectedSecretName, resultSecretName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -27,10 +26,6 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
SecretNamePrefix = "higress-secret-"
|
||||
)
|
||||
|
||||
type SecretMgr struct {
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
@@ -46,13 +41,21 @@ func NewSecretMgr(namespace string, client kubernetes.Interface) (*SecretMgr, er
|
||||
}
|
||||
|
||||
func (s *SecretMgr) Update(domain string, secretName string, privateKey []byte, certificate []byte, notBefore time.Time, notAfter time.Time, isRenew bool) error {
|
||||
//secretName := s.getSecretName(domain)
|
||||
secret := s.constructSecret(domain, privateKey, certificate, notBefore, notAfter, isRenew)
|
||||
_, err := s.client.CoreV1().Secrets(s.namespace).Get(context.Background(), secretName, metav1.GetOptions{})
|
||||
CertLog.Infof("update secret, domain:%s, secretName:%s, notBefore:%v, notAfter:%v, isRenew:%t", domain, secretName, notBefore, notAfter, isRenew)
|
||||
name := secretName
|
||||
namespace := s.namespace
|
||||
namespaceP, secretP := ParseTLSSecret(secretName)
|
||||
if namespaceP != "" {
|
||||
namespace = namespaceP
|
||||
name = secretP
|
||||
}
|
||||
|
||||
secret := s.constructSecret(domain, name, namespace, privateKey, certificate, notBefore, notAfter, isRenew)
|
||||
_, err := s.client.CoreV1().Secrets(namespace).Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
// create secret
|
||||
_, err2 := s.client.CoreV1().Secrets(s.namespace).Create(context.Background(), secret, metav1.CreateOptions{})
|
||||
_, err2 := s.client.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{})
|
||||
return err2
|
||||
}
|
||||
return err
|
||||
@@ -61,7 +64,7 @@ func (s *SecretMgr) Update(domain string, secretName string, privateKey []byte,
|
||||
if _, ok := secret.Annotations["higress.io/cert-domain"]; !ok {
|
||||
return fmt.Errorf("the secret name %s is not automatic https secret name for the domain:%s, please rename it in config", secretName, domain)
|
||||
}
|
||||
_, err1 := s.client.CoreV1().Secrets(s.namespace).Update(context.Background(), secret, metav1.UpdateOptions{})
|
||||
_, err1 := s.client.CoreV1().Secrets(namespace).Update(context.Background(), secret, metav1.UpdateOptions{})
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
@@ -69,23 +72,13 @@ func (s *SecretMgr) Update(domain string, secretName string, privateKey []byte,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SecretMgr) Delete(domain string) error {
|
||||
secretName := s.getSecretName(domain)
|
||||
err := s.client.CoreV1().Secrets(s.namespace).Delete(context.Background(), secretName, metav1.DeleteOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SecretMgr) getSecretName(domain string) string {
|
||||
return SecretNamePrefix + strings.ReplaceAll(strings.TrimSpace(domain), ".", "-")
|
||||
}
|
||||
|
||||
func (s *SecretMgr) constructSecret(domain string, privateKey []byte, certificate []byte, notBefore time.Time, notAfter time.Time, isRenew bool) *v1.Secret {
|
||||
secretName := s.getSecretName(domain)
|
||||
func (s *SecretMgr) constructSecret(domain string, name string, namespace string, privateKey []byte, certificate []byte, notBefore time.Time, notAfter time.Time, isRenew bool) *v1.Secret {
|
||||
annotationMap := make(map[string]string, 0)
|
||||
annotationMap["higress.io/cert-domain"] = domain
|
||||
annotationMap["higress.io/cert-notAfter"] = notAfter.Format("2006-01-02 15:04:05")
|
||||
annotationMap["higress.io/cert-notBefore"] = notBefore.Format("2006-01-02 15:04:05")
|
||||
annotationMap["higress.io/cert-renew"] = strconv.FormatBool(isRenew)
|
||||
annotationMap["higress.io/cert-source"] = string(IssuerTypeLetsencrypt)
|
||||
if isRenew {
|
||||
annotationMap["higress.io/cert-renew-time"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -97,8 +90,8 @@ func (s *SecretMgr) constructSecret(domain string, privateKey []byte, certificat
|
||||
dataMap["tls.crt"] = certificate
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: s.namespace,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotationMap,
|
||||
},
|
||||
Type: v1.SecretTypeTLS,
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
@@ -37,12 +38,14 @@ type Server struct {
|
||||
clientSet kubernetes.Interface
|
||||
controller *Controller
|
||||
certMgr *CertMgr
|
||||
XDSUpdater model.XDSUpdater
|
||||
}
|
||||
|
||||
func NewServer(clientSet kubernetes.Interface, opts *Option) (*Server, error) {
|
||||
func NewServer(clientSet kubernetes.Interface, XDSUpdater model.XDSUpdater, opts *Option) (*Server, error) {
|
||||
server := &Server{
|
||||
clientSet: clientSet,
|
||||
opts: opts,
|
||||
clientSet: clientSet,
|
||||
opts: opts,
|
||||
XDSUpdater: XDSUpdater,
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
@@ -65,7 +68,7 @@ func (s *Server) InitServer() error {
|
||||
return err
|
||||
}
|
||||
// init certmgr
|
||||
certMgr, err := InitCertMgr(s.opts, s.clientSet, defaultConfig) // config and start
|
||||
certMgr, err := InitCertMgr(s.opts, s.clientSet, defaultConfig, s.XDSUpdater, configMgr) // config and start
|
||||
s.certMgr = certMgr
|
||||
// init controller
|
||||
controller, err := NewController(s.clientSet, s.opts.Namespace, certMgr, configMgr)
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CertificatesPrefix = "/certificates"
|
||||
CertificatesPrefix = "certificates"
|
||||
ConfigmapStoreCertficatesPrefix = "higress-cert-store-certificates-"
|
||||
ConfigmapStoreDefaultName = "higress-cert-store-default"
|
||||
)
|
||||
@@ -155,7 +155,7 @@ func (s *ConfigmapStorage) List(ctx context.Context, prefix string, recursive bo
|
||||
// Check if the prefix corresponds to a specific key
|
||||
hashPrefix := fastHash([]byte(prefix))
|
||||
if strings.HasPrefix(prefix, CertificatesPrefix) {
|
||||
// If the prefix is "/certificates", get all ConfigMaps and traverse each one
|
||||
// If the prefix is "certificates/", get all ConfigMaps and traverse each one
|
||||
// List all ConfigMaps in the namespace with label higress.io/cert-https=true
|
||||
configmaps, err := s.client.CoreV1().ConfigMaps(s.namespace).List(ctx, metav1.ListOptions{FieldSelector: "metadata.annotations['higress.io/cert-https'] == 'true'"})
|
||||
if err != nil {
|
||||
@@ -289,14 +289,29 @@ func (s *ConfigmapStorage) String() string {
|
||||
return "ConfigmapStorage"
|
||||
}
|
||||
|
||||
// getConfigmapStoreNameByKey determines the storage name for a given key.
|
||||
// It checks if the key starts with 'certificates/' and if so, the key pattern should match one of the following:
|
||||
// 'certificates/<issuerKey>/<domain>/<domain>.json',
|
||||
// 'certificates/<issuerKey>/<domain>/<domain>.crt',
|
||||
// or 'certificates/<issuerKey>/<domain>/<domain>.key'.
|
||||
// It then returns the corresponding ConfigMap name.
|
||||
// If the key does not start with 'certificates/', it returns the default store name.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// key - The configuration map key that needs to be mapped to a storage name.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// string - The calculated or default storage name based on the key.
|
||||
func (s *ConfigmapStorage) getConfigmapStoreNameByKey(key string) string {
|
||||
parts := strings.SplitN(key, "/", 10)
|
||||
if len(parts) >= 4 && parts[1] == "certificates" {
|
||||
domain := strings.TrimSuffix(parts[3], ".crt")
|
||||
domain = strings.TrimSuffix(domain, ".key")
|
||||
domain = strings.TrimSuffix(domain, ".json")
|
||||
issuerKey := parts[2]
|
||||
return ConfigmapStoreCertficatesPrefix + fastHash([]byte(issuerKey+domain))
|
||||
if strings.HasPrefix(key, "certificates/") {
|
||||
parts := strings.Split(key, "/")
|
||||
if len(parts) >= 4 && parts[0] == "certificates" {
|
||||
domain := parts[2]
|
||||
issuerKey := parts[1]
|
||||
return ConfigmapStoreCertficatesPrefix + fastHash([]byte(issuerKey+domain))
|
||||
}
|
||||
}
|
||||
return ConfigmapStoreDefaultName
|
||||
}
|
||||
|
||||
@@ -39,22 +39,29 @@ func TestGetConfigmapStoreNameByKey(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "certificate crt",
|
||||
key: "/certificates/issuerKey/domain.crt",
|
||||
key: "certificates/issuerKey/domain/domain.crt",
|
||||
expected: "higress-cert-store-certificates-" + fastHash([]byte("issuerKey"+"domain")),
|
||||
},
|
||||
|
||||
{
|
||||
name: "47.237.14.136.sslip.io crt",
|
||||
key: "certificates/acme-v02.api.letsencrypt.org-directory/47.237.14.136.sslip.io/47.237.14.136.sslip.io.crt",
|
||||
expected: "higress-cert-store-certificates-" + fastHash([]byte("acme-v02.api.letsencrypt.org-directory"+"47.237.14.136.sslip.io")),
|
||||
},
|
||||
|
||||
{
|
||||
name: "certificate meta",
|
||||
key: "/certificates/issuerKey/domain.json",
|
||||
key: "certificates/issuerKey/domain/domain.json",
|
||||
expected: "higress-cert-store-certificates-" + fastHash([]byte("issuerKey"+"domain")),
|
||||
},
|
||||
{
|
||||
name: "certificate key",
|
||||
key: "/certificates/issuerKey/domain.key",
|
||||
key: "certificates/issuerKey/domain/domain.key",
|
||||
expected: "higress-cert-store-certificates-" + fastHash([]byte("issuerKey"+"domain")),
|
||||
},
|
||||
{
|
||||
name: "user key",
|
||||
key: "/users/hello/2",
|
||||
key: "users/hello/2",
|
||||
expected: "higress-cert-store-default",
|
||||
},
|
||||
{
|
||||
@@ -82,7 +89,7 @@ func TestExists(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Store a test key
|
||||
testKey := "/certificates/issuer1/domain1.crt"
|
||||
testKey := "certificates/issuer1/domain1/domain1.crt"
|
||||
err = storage.Store(context.Background(), testKey, []byte("test-data"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -94,17 +101,17 @@ func TestExists(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Existing Key",
|
||||
key: "/certificates/issuer1/domain1.crt",
|
||||
key: "certificates/issuer1/domain1/domain1.crt",
|
||||
shouldExist: true,
|
||||
},
|
||||
{
|
||||
name: "Non-Existent Key1",
|
||||
key: "/certificates/issuer2/domain2.crt",
|
||||
key: "certificates/issuer2/domain2/domain2.crt",
|
||||
shouldExist: false,
|
||||
},
|
||||
{
|
||||
name: "Non-Existent Key2",
|
||||
key: "/users/hello/a",
|
||||
key: "users/hello/a",
|
||||
shouldExist: false,
|
||||
},
|
||||
// Add more test cases as needed
|
||||
@@ -129,7 +136,7 @@ func TestLoad(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Store a test key
|
||||
testKey := "/certificates/issuer1/domain1.crt"
|
||||
testKey := "certificates/issuer1/domain1/domain1.crt"
|
||||
testValue := []byte("test-data")
|
||||
err = storage.Store(context.Background(), testKey, testValue)
|
||||
assert.NoError(t, err)
|
||||
@@ -143,13 +150,13 @@ func TestLoad(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Existing Key",
|
||||
key: "/certificates/issuer1/domain1.crt",
|
||||
key: "certificates/issuer1/domain1/domain1.crt",
|
||||
expected: testValue,
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Non-Existent Key",
|
||||
key: "/certificates/issuer2/domain2.crt",
|
||||
key: "certificates/issuer2/domain2/domain2.crt",
|
||||
expected: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
@@ -192,28 +199,28 @@ func TestStore(t *testing.T) {
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "Store Key with /certificates prefix",
|
||||
key: "/certificates/issuer1/domain1.crt",
|
||||
name: "Store Key with certificates prefix",
|
||||
key: "certificates/issuer1/domain1/domain1.crt",
|
||||
value: []byte("test-data1"),
|
||||
expected: map[string]string{fastHash([]byte("/certificates/issuer1/domain1.crt")): `{"k":"/certificates/issuer1/domain1.crt","v":"dGVzdC1kYXRhMQ=="}`},
|
||||
expected: map[string]string{fastHash([]byte("certificates/issuer1/domain1/domain1.crt")): `{"k":"certificates/issuer1/domain1/domain1.crt","v":"dGVzdC1kYXRhMQ=="}`},
|
||||
expectedConfigmapName: "higress-cert-store-certificates-" + fastHash([]byte("issuer1"+"domain1")),
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Store Key with /certificates prefix (additional data)",
|
||||
key: "/certificates/issuer2/domain2.crt",
|
||||
name: "Store Key with certificates prefix (additional data)",
|
||||
key: "certificates/issuer2/domain2/domain2.crt",
|
||||
value: []byte("test-data2"),
|
||||
expected: map[string]string{
|
||||
fastHash([]byte("/certificates/issuer2/domain2.crt")): `{"k":"/certificates/issuer2/domain2.crt","v":"dGVzdC1kYXRhMg=="}`,
|
||||
fastHash([]byte("certificates/issuer2/domain2/domain2.crt")): `{"k":"certificates/issuer2/domain2/domain2.crt","v":"dGVzdC1kYXRhMg=="}`,
|
||||
},
|
||||
expectedConfigmapName: "higress-cert-store-certificates-" + fastHash([]byte("issuer2"+"domain2")),
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Store Key without /certificates prefix",
|
||||
key: "/other/path/data.txt",
|
||||
name: "Store Key without certificates prefix",
|
||||
key: "other/path/data.txt",
|
||||
value: []byte("test-data3"),
|
||||
expected: map[string]string{fastHash([]byte("/other/path/data.txt")): `{"k":"/other/path/data.txt","v":"dGVzdC1kYXRhMw=="}`},
|
||||
expected: map[string]string{fastHash([]byte("other/path/data.txt")): `{"k":"other/path/data.txt","v":"dGVzdC1kYXRhMw=="}`},
|
||||
expectedConfigmapName: "higress-cert-store-default",
|
||||
shouldError: false,
|
||||
},
|
||||
@@ -256,17 +263,17 @@ func TestList(t *testing.T) {
|
||||
// Store some test data
|
||||
// Store some test data
|
||||
testKeys := []string{
|
||||
"/certificates/issuer1/domain1.crt",
|
||||
"/certificates/issuer1/domain2.crt",
|
||||
"/certificates/issuer1/domain3.crt", // Added another domain for issuer1
|
||||
"/certificates/issuer2/domain4.crt",
|
||||
"/certificates/issuer2/domain5.crt",
|
||||
"/certificates/issuer3/subdomain1/domain6.crt", // Two-level subdirectory under issuer3
|
||||
"/certificates/issuer3/subdomain1/subdomain2/domain7.crt", // Two more levels under issuer3
|
||||
"/other-prefix/key1/file1",
|
||||
"/other-prefix/key1/file2",
|
||||
"/other-prefix/key2/file3",
|
||||
"/other-prefix/key2/file4",
|
||||
"certificates/issuer1/domain1/domain1.crt",
|
||||
"certificates/issuer1/domain2/domain2.crt",
|
||||
"certificates/issuer1/domain3/domain3.crt", // Added another domain for issuer1
|
||||
"certificates/issuer2/domain4/domain4.crt",
|
||||
"certificates/issuer2/domain5/domain5.crt",
|
||||
"certificates/issuer3/domain6/domain6.crt", // Two-level subdirectory under issuer3
|
||||
"certificates/issuer3/subdomain1/subdomain2/domain7.crt", // Two more levels under issuer3
|
||||
"other-prefix/key1/file1",
|
||||
"other-prefix/key1/file2",
|
||||
"other-prefix/key2/file3",
|
||||
"other-prefix/key2/file4",
|
||||
}
|
||||
|
||||
for _, key := range testKeys {
|
||||
@@ -283,34 +290,34 @@ func TestList(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "List Certificates (Non-Recursive)",
|
||||
prefix: "/certificates",
|
||||
prefix: "certificates",
|
||||
recursive: false,
|
||||
expected: []string{"/certificates/issuer1", "/certificates/issuer2", "/certificates/issuer3"},
|
||||
expected: []string{"certificates/issuer1", "certificates/issuer2", "certificates/issuer3"},
|
||||
},
|
||||
{
|
||||
name: "List Certificates (Recursive)",
|
||||
prefix: "/certificates",
|
||||
prefix: "certificates",
|
||||
recursive: true,
|
||||
expected: []string{"/certificates/issuer1/domain1.crt", "/certificates/issuer1/domain2.crt", "/certificates/issuer1/domain3.crt", "/certificates/issuer2/domain4.crt", "/certificates/issuer2/domain5.crt", "/certificates/issuer3/subdomain1/domain6.crt", "/certificates/issuer3/subdomain1/subdomain2/domain7.crt"},
|
||||
expected: []string{"certificates/issuer1/domain1/domain1.crt", "certificates/issuer1/domain2/domain2.crt", "certificates/issuer1/domain3/domain3.crt", "certificates/issuer2/domain4/domain4.crt", "certificates/issuer2/domain5/domain5.crt", "certificates/issuer3/domain6/domain6.crt", "certificates/issuer3/subdomain1/subdomain2/domain7.crt"},
|
||||
},
|
||||
{
|
||||
name: "List Other Prefix (Non-Recursive)",
|
||||
prefix: "/other-prefix",
|
||||
prefix: "other-prefix",
|
||||
recursive: false,
|
||||
expected: []string{"/other-prefix/key1", "/other-prefix/key2"},
|
||||
expected: []string{"other-prefix/key1", "other-prefix/key2"},
|
||||
},
|
||||
|
||||
{
|
||||
name: "List Other Prefix (Non-Recursive)",
|
||||
prefix: "/other-prefix/key1",
|
||||
prefix: "other-prefix/key1",
|
||||
recursive: false,
|
||||
expected: []string{"/other-prefix/key1/file1", "/other-prefix/key1/file2"},
|
||||
expected: []string{"other-prefix/key1/file1", "other-prefix/key1/file2"},
|
||||
},
|
||||
{
|
||||
name: "List Other Prefix (Recursive)",
|
||||
prefix: "/other-prefix",
|
||||
prefix: "other-prefix",
|
||||
recursive: true,
|
||||
expected: []string{"/other-prefix/key1/file1", "/other-prefix/key1/file2", "/other-prefix/key2/file3", "/other-prefix/key2/file4"},
|
||||
expected: []string{"other-prefix/key1/file1", "other-prefix/key1/file2", "other-prefix/key2/file3", "other-prefix/key2/file4"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ func openCommand(writer io.Writer, command string, args ...string) {
|
||||
_, err := exec.LookPath(command)
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
fmt.Fprintf(writer, "Could not open your browser. Please open it maually.\n")
|
||||
fmt.Fprintf(writer, "Could not open your browser. Please open it manually.\n")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", args[0], err.Error())
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
const (
|
||||
setFlagHelpStr = `Override an higress profile value, e.g. to choose a profile
|
||||
(--set profile=local-k8s), or override profile values (--set gateway.replicas=2), or override helm values (--set values.global.proxy.resources.requsts.cpu=500m).`
|
||||
(--set profile=local-k8s), or override profile values (--set gateway.replicas=2), or override helm values (--set values.global.proxy.resources.requests.cpu=500m).`
|
||||
// manifestsFlagHelpStr is the command line description for --manifests
|
||||
manifestsFlagHelpStr = `Specify a path to a directory of profiles
|
||||
(e.g. ~/Downloads/higress/manifests).`
|
||||
@@ -101,7 +101,7 @@ func newInstallCmd() *cobra.Command {
|
||||
hgctl install --set profile=local-k8s --set global.enableIstioAPI=true --set gateway.replicas=2"
|
||||
|
||||
# To override helm setting
|
||||
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requsts.cpu=500m"
|
||||
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requests.cpu=500m"
|
||||
|
||||
|
||||
`,
|
||||
@@ -175,7 +175,7 @@ func promptInstall(writer io.Writer, profileName string) bool {
|
||||
|
||||
func promptProfileName(writer io.Writer) string {
|
||||
answer := ""
|
||||
fmt.Fprintf(writer, "\nPlease select higress install configration profile:\n")
|
||||
fmt.Fprintf(writer, "\nPlease select higress install configuration profile:\n")
|
||||
fmt.Fprintf(writer, "\n1.Install higress to local kubernetes cluster like kind etc.\n")
|
||||
fmt.Fprintf(writer, "\n2.Install higress to kubernetes cluster\n")
|
||||
fmt.Fprintf(writer, "\n3.Install higress to local docker environment\n")
|
||||
|
||||
@@ -176,7 +176,7 @@ func (a *Agent) checkSudoPermission() error {
|
||||
case <-time.After(5 * time.Second):
|
||||
cmd2.Process.Signal(os.Interrupt)
|
||||
if !a.quiet {
|
||||
fmt.Fprintf(a.writer, "checked result: timeout execeed and need sudo with password\n")
|
||||
fmt.Fprintf(a.writer, "checked result: timeout exceed and need sudo with password\n")
|
||||
}
|
||||
a.runSudoState = SudoWithPassword
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ func upgrade(writer io.Writer, iArgs *InstallArgs) error {
|
||||
func promptUpgrade(writer io.Writer) bool {
|
||||
answer := ""
|
||||
for {
|
||||
fmt.Fprintf(writer, "All Higress resources will be upgraed from the cluster. \nProceed? (y/N)")
|
||||
fmt.Fprintf(writer, "All Higress resources will be upgrade from the cluster. \nProceed? (y/N)")
|
||||
fmt.Scanln(&answer)
|
||||
if strings.TrimSpace(answer) == "y" {
|
||||
fmt.Fprintf(writer, "\n")
|
||||
@@ -170,7 +170,7 @@ func promptProfileContexts(writer io.Writer, profileContexts []*installer.Profil
|
||||
if len(profileContexts) == 1 {
|
||||
fmt.Fprintf(writer, "\nFound a profile:: ")
|
||||
} else {
|
||||
fmt.Fprintf(writer, "\nPlease select higress installed configration profiles:\n")
|
||||
fmt.Fprintf(writer, "\nPlease select higress installed configuration profiles:\n")
|
||||
}
|
||||
index := 1
|
||||
for _, profileContext := range profileContexts {
|
||||
|
||||
@@ -32,7 +32,7 @@ func ParseProtocol(s string) Protocol {
|
||||
return TCP
|
||||
case "http":
|
||||
return HTTP
|
||||
case "grpc":
|
||||
case "grpc", "triple", "tri":
|
||||
return GRPC
|
||||
case "dubbo":
|
||||
return Dubbo
|
||||
|
||||
@@ -841,6 +841,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
StructValue: rule.Config,
|
||||
}
|
||||
var matchItems []*types.Value
|
||||
// match ingress
|
||||
for _, ing := range rule.Ingress {
|
||||
matchItems = append(matchItems, &types.Value{
|
||||
Kind: &types.Value_StringValue{
|
||||
@@ -861,6 +862,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
})
|
||||
continue
|
||||
}
|
||||
// match domain
|
||||
for _, domain := range rule.Domain {
|
||||
matchItems = append(matchItems, &types.Value{
|
||||
Kind: &types.Value_StringValue{
|
||||
@@ -868,10 +870,31 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(matchItems) > 0 {
|
||||
v.StructValue.Fields["_match_domain_"] = &types.Value{
|
||||
Kind: &types.Value_ListValue{
|
||||
ListValue: &types.ListValue{
|
||||
Values: matchItems,
|
||||
},
|
||||
},
|
||||
}
|
||||
ruleValues = append(ruleValues, &types.Value{
|
||||
Kind: v,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// match service
|
||||
for _, service := range rule.Service {
|
||||
matchItems = append(matchItems, &types.Value{
|
||||
Kind: &types.Value_StringValue{
|
||||
StringValue: service,
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(matchItems) == 0 {
|
||||
return nil, fmt.Errorf("invalid match rule has no match condition, rule:%v", rule)
|
||||
}
|
||||
v.StructValue.Fields["_match_domain_"] = &types.Value{
|
||||
v.StructValue.Fields["_match_service_"] = &types.Value{
|
||||
Kind: &types.Value_ListValue{
|
||||
ListValue: &types.ListValue{
|
||||
Values: matchItems,
|
||||
@@ -918,7 +941,7 @@ func (m *IngressConfig) AddOrUpdateWasmPlugin(clusterNamespacedName util.Cluster
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
for _, f := range m.wasmPluginHandlers {
|
||||
IngressLog.Debug("WasmPlugin triggerd update")
|
||||
IngressLog.Debug("WasmPlugin triggered update")
|
||||
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, model.EventUpdate)
|
||||
}
|
||||
istioWasmPlugin, err := m.convertIstioWasmPlugin(&wasmPlugin.Spec)
|
||||
@@ -960,7 +983,7 @@ func (m *IngressConfig) DeleteWasmPlugin(clusterNamespacedName util.ClusterNames
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
for _, f := range m.wasmPluginHandlers {
|
||||
IngressLog.Debug("WasmPlugin triggerd update")
|
||||
IngressLog.Debug("WasmPlugin triggered update")
|
||||
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, model.EventDelete)
|
||||
}
|
||||
}
|
||||
@@ -987,7 +1010,7 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
for _, f := range m.serviceEntryHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd serviceEntry update")
|
||||
IngressLog.Debug("McpBridge triggered serviceEntry update")
|
||||
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, model.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace)
|
||||
@@ -1042,7 +1065,7 @@ func (m *IngressConfig) AddOrUpdateHttp2Rpc(clusterNamespacedName util.ClusterNa
|
||||
}
|
||||
|
||||
func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
IngressLog.Infof("Http2Rpc triggerd deleted event %s", clusterNamespacedName.Name)
|
||||
IngressLog.Infof("Http2Rpc triggered deleted event %s", clusterNamespacedName.Name)
|
||||
if clusterNamespacedName.Namespace != m.namespace {
|
||||
return
|
||||
}
|
||||
@@ -1054,7 +1077,7 @@ func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespa
|
||||
}
|
||||
m.mutex.Unlock()
|
||||
if hit {
|
||||
IngressLog.Infof("Http2Rpc triggerd deleted event executed %s", clusterNamespacedName.Name)
|
||||
IngressLog.Infof("Http2Rpc triggered deleted event executed %s", clusterNamespacedName.Name)
|
||||
push := func(kind config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&model.PushRequest{
|
||||
Full: true,
|
||||
@@ -1160,13 +1183,13 @@ func (m *IngressConfig) constructHttp2RpcEnvoyFilter(http2rpcConfig *annotations
|
||||
IngressLog.Infof("Found http2rpc mappings %v", mappings)
|
||||
if _, exist := mappings[http2rpcConfig.Name]; !exist {
|
||||
IngressLog.Errorf("Http2RpcConfig name %s, not found Http2Rpc CRD", http2rpcConfig.Name)
|
||||
return nil, errors.New("invalid http2rpcConfig has no useable http2rpc")
|
||||
return nil, errors.New("invalid http2rpcConfig has no usable http2rpc")
|
||||
}
|
||||
http2rpcCRD := mappings[http2rpcConfig.Name]
|
||||
|
||||
if http2rpcCRD.GetDubbo() == nil {
|
||||
IngressLog.Errorf("Http2RpcConfig name %s, only support Http2Rpc CRD Dubbo Service type", http2rpcConfig.Name)
|
||||
return nil, errors.New("invalid http2rpcConfig has no useable http2rpc")
|
||||
return nil, errors.New("invalid http2rpcConfig has no usable http2rpc")
|
||||
}
|
||||
|
||||
httpRoute := route.HTTPRoute
|
||||
@@ -1293,7 +1316,7 @@ func (m *IngressConfig) constructHttp2RpcMethods(dubbo *higressv1.DubboService)
|
||||
var method = make(map[string]interface{})
|
||||
method["name"] = serviceMethod.GetServiceMethod()
|
||||
var params []interface{}
|
||||
// paramFromEntireBody is for methods with single parameter. So when paramFromEntireBody exists, we just ignore parmas.
|
||||
// paramFromEntireBody is for methods with single parameter. So when paramFromEntireBody exists, we just ignore params.
|
||||
var paramFromEntireBody = serviceMethod.GetParamFromEntireBody()
|
||||
if paramFromEntireBody != nil {
|
||||
var param = make(map[string]interface{})
|
||||
|
||||
@@ -431,8 +431,16 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
// If there is no matching secret, try to get it from configmap.
|
||||
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
matchSecretName := httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
if matchSecretName != "" {
|
||||
namespace, secret := cert.ParseTLSSecret(matchSecretName)
|
||||
if namespace == "" {
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
} else {
|
||||
secretNamespace = namespace
|
||||
}
|
||||
secretName = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -441,6 +449,11 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
|
||||
if httpsCredentialConfig != nil {
|
||||
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
namespace, secret := cert.ParseTLSSecret(secretName)
|
||||
if namespace != "" {
|
||||
secretNamespace = namespace
|
||||
secretName = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
if secretName == "" {
|
||||
|
||||
@@ -417,8 +417,16 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
// If there is no matching secret, try to get it from configmap.
|
||||
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
matchSecretName := httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
if matchSecretName != "" {
|
||||
namespace, secret := cert.ParseTLSSecret(matchSecretName)
|
||||
if namespace == "" {
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
} else {
|
||||
secretNamespace = namespace
|
||||
}
|
||||
secretName = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,6 +435,11 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
|
||||
if httpsCredentialConfig != nil {
|
||||
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
namespace, secret := cert.ParseTLSSecret(secretName)
|
||||
if namespace != "" {
|
||||
secretNamespace = namespace
|
||||
secretName = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,6 @@ func (c *controller) processNextWorkItem() bool {
|
||||
func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
event := model.EventUpdate
|
||||
ing, err := c.ingressLister.Ingresses(namespacedName.Namespace).Get(namespacedName.Name)
|
||||
ing.Status.InitializeConditions()
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
event = model.EventDelete
|
||||
@@ -181,6 +180,8 @@ func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ing.Status.InitializeConditions()
|
||||
|
||||
// we should check need process only when event is not delete,
|
||||
// if it is delete event, and previously processed, we need to process too.
|
||||
if event != model.EventDelete {
|
||||
|
||||
53
plugins/wasm-assemblyscript/README.md
Normal file
53
plugins/wasm-assemblyscript/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
## 介绍
|
||||
|
||||
此 SDK 用于使用 AssemblyScript 语言开发 Higress 的 Wasm 插件。
|
||||
|
||||
### 如何使用SDK
|
||||
|
||||
创建一个新的 AssemblyScript 项目。
|
||||
|
||||
```
|
||||
npm init
|
||||
npm install --save-dev assemblyscript
|
||||
npx asinit .
|
||||
```
|
||||
|
||||
在asconfig.json文件中,作为传递给asc编译器的选项之一,包含"use": "abort=abort_proc_exit"。
|
||||
|
||||
```
|
||||
{
|
||||
"options": {
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将`"@higress/wasm-assemblyscript": "^0.0.4"`添加到你的依赖项中,然后运行`npm install`。
|
||||
|
||||
### 本地构建
|
||||
|
||||
```
|
||||
npm run asbuild
|
||||
```
|
||||
|
||||
构建结果将在`build`文件夹中。其中,`debug.wasm`和`release.wasm`是已编译的文件,在生产环境中建议使用`release.wasm`。
|
||||
|
||||
注:如果需要插件带有 name section 信息需要带上`"debug": true`,编译参数解释详见[using-the-compiler](https://www.assemblyscript.org/compiler.html#using-the-compiler)。
|
||||
|
||||
```json
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false,
|
||||
"debug": true
|
||||
}
|
||||
```
|
||||
|
||||
### AssemblyScript 限制
|
||||
|
||||
此 SDK 使用的 AssemblyScript 版本为`0.27.29`,参考[AssemblyScript Status](https://www.assemblyscript.org/status.html)该版本尚未支持闭包、异常、迭代器等特性,并且JSON,正则表达式等功能还尚未在标准库中实现,暂时需要使用社区提供的实现。
|
||||
|
||||
23
plugins/wasm-assemblyscript/asconfig.json
Normal file
23
plugins/wasm-assemblyscript/asconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "build/debug.wasm",
|
||||
"textFile": "build/debug.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"bindings": "esm",
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
214
plugins/wasm-assemblyscript/assembly/cluster_wrapper.ts
Normal file
214
plugins/wasm-assemblyscript/assembly/cluster_wrapper.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
log,
|
||||
LogLevelValues,
|
||||
get_property,
|
||||
WasmResultValues,
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
import { getRequestHost } from "./request_wrapper";
|
||||
|
||||
export abstract class Cluster {
|
||||
abstract clusterName(): string;
|
||||
abstract hostName(): string;
|
||||
}
|
||||
|
||||
export class RouteCluster extends Cluster {
|
||||
host: string;
|
||||
constructor(host: string = "") {
|
||||
super();
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
let result = get_property("cluster_name");
|
||||
if (result.status != WasmResultValues.Ok) {
|
||||
log(LogLevelValues.error, "get route cluster failed");
|
||||
return "";
|
||||
}
|
||||
return String.UTF8.decode(result.returnValue);
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return getRequestHost();
|
||||
}
|
||||
}
|
||||
|
||||
export class K8sCluster extends Cluster {
|
||||
serviceName: string;
|
||||
namespace: string;
|
||||
port: i64;
|
||||
version: string;
|
||||
host: string;
|
||||
|
||||
constructor(
|
||||
serviceName: string,
|
||||
namespace: string,
|
||||
port: i64,
|
||||
version: string = "",
|
||||
host: string = ""
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.namespace = namespace;
|
||||
this.port = port;
|
||||
this.version = version;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
let namespace = this.namespace != "" ? this.namespace : "default";
|
||||
return `outbound|${this.port}|${this.version}|${this.serviceName}.${namespace}.svc.cluster.local`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return `${this.serviceName}.${this.namespace}.svc.cluster.local`;
|
||||
}
|
||||
}
|
||||
|
||||
export class NacosCluster extends Cluster {
|
||||
serviceName: string;
|
||||
group: string;
|
||||
namespaceID: string;
|
||||
port: i64;
|
||||
isExtRegistry: boolean;
|
||||
version: string;
|
||||
host: string;
|
||||
|
||||
constructor(
|
||||
serviceName: string,
|
||||
namespaceID: string,
|
||||
port: i64,
|
||||
// use DEFAULT-GROUP by default
|
||||
group: string = "DEFAULT-GROUP",
|
||||
// set true if use edas/sae registry
|
||||
isExtRegistry: boolean = false,
|
||||
version: string = "",
|
||||
host: string = ""
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.group = group.replace("_", "-");
|
||||
this.namespaceID = namespaceID;
|
||||
this.port = port;
|
||||
this.isExtRegistry = isExtRegistry;
|
||||
this.version = version;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
let tail = "nacos" + (this.isExtRegistry ? "-ext" : "");
|
||||
return `outbound|${this.port}|${this.version}|${this.serviceName}.${this.group}.${this.namespaceID}.${tail}`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
export class StaticIpCluster extends Cluster {
|
||||
serviceName: string;
|
||||
port: i64;
|
||||
host: string;
|
||||
|
||||
constructor(serviceName: string, port: i64, host: string = "") {
|
||||
super()
|
||||
this.serviceName = serviceName;
|
||||
this.port = port;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.serviceName}.static`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
export class DnsCluster extends Cluster {
|
||||
serviceName: string;
|
||||
domain: string;
|
||||
port: i64;
|
||||
|
||||
constructor(serviceName: string, domain: string, port: i64) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.domain = domain;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.serviceName}.dns`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
return this.domain;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsulCluster extends Cluster {
|
||||
serviceName: string;
|
||||
datacenter: string;
|
||||
port: i64;
|
||||
host: string;
|
||||
|
||||
constructor(
|
||||
serviceName: string,
|
||||
datacenter: string,
|
||||
port: i64,
|
||||
host: string = ""
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.datacenter = datacenter;
|
||||
this.port = port;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.serviceName}.${this.datacenter}.consul`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
export class FQDNCluster extends Cluster {
|
||||
fqdn: string;
|
||||
host: string;
|
||||
port: i64;
|
||||
|
||||
constructor(fqdn: string, port: i64, host: string = "") {
|
||||
super();
|
||||
this.fqdn = fqdn;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.fqdn}`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.fqdn;
|
||||
}
|
||||
}
|
||||
120
plugins/wasm-assemblyscript/assembly/http_wrapper.ts
Normal file
120
plugins/wasm-assemblyscript/assembly/http_wrapper.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Cluster
|
||||
} from "./cluster_wrapper"
|
||||
|
||||
import {
|
||||
log,
|
||||
LogLevelValues,
|
||||
Headers,
|
||||
HeaderPair,
|
||||
root_context,
|
||||
BufferTypeValues,
|
||||
get_buffer_bytes,
|
||||
BaseContext,
|
||||
stream_context,
|
||||
WasmResultValues,
|
||||
RootContext,
|
||||
ResponseCallBack
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
|
||||
export interface HttpClient {
|
||||
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
}
|
||||
|
||||
const methodArrayBuffer: ArrayBuffer = String.UTF8.encode(":method");
|
||||
const pathArrayBuffer: ArrayBuffer = String.UTF8.encode(":path");
|
||||
const authorityArrayBuffer: ArrayBuffer = String.UTF8.encode(":authority");
|
||||
|
||||
const StatusBadGateway: i32 = 502;
|
||||
|
||||
export class ClusterClient {
|
||||
cluster: Cluster;
|
||||
|
||||
constructor(cluster: Cluster) {
|
||||
this.cluster = cluster;
|
||||
}
|
||||
|
||||
private httpCall(method: string, path: string, headers: Headers, body: ArrayBuffer, callback: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
if (root_context == null) {
|
||||
log(LogLevelValues.error, "Root context is null");
|
||||
return false;
|
||||
}
|
||||
for (let i: i32 = headers.length - 1; i >= 0; i--) {
|
||||
const key = String.UTF8.decode(headers[i].key)
|
||||
if ((key == ":method") || (key == ":path") || (key == ":authority")) {
|
||||
headers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
headers.push(new HeaderPair(methodArrayBuffer, String.UTF8.encode(method)));
|
||||
headers.push(new HeaderPair(pathArrayBuffer, String.UTF8.encode(path)));
|
||||
headers.push(new HeaderPair(authorityArrayBuffer, String.UTF8.encode(this.cluster.hostName())));
|
||||
|
||||
const result = (root_context as RootContext).httpCall(this.cluster.clusterName(), headers, body, [], timeoutMillisecond, root_context as BaseContext, callback,
|
||||
(_origin_context: BaseContext, _numHeaders: u32, body_size: usize, _trailers: u32, callback: ResponseCallBack): void => {
|
||||
const respBody = get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, 0, body_size as u32);
|
||||
const respHeaders = stream_context.headers.http_callback.get_headers()
|
||||
let code = StatusBadGateway;
|
||||
let headers = new Array<HeaderPair>();
|
||||
for (let i = 0; i < respHeaders.length; i++) {
|
||||
const h = respHeaders[i];
|
||||
if (String.UTF8.decode(h.key) == ":status") {
|
||||
code = <i32>parseInt(String.UTF8.decode(h.value))
|
||||
}
|
||||
headers.push(new HeaderPair(h.key, h.value));
|
||||
}
|
||||
log(LogLevelValues.debug, `http call end, code: ${code}, body: ${String.UTF8.decode(respBody)}`)
|
||||
callback(code, headers, respBody);
|
||||
})
|
||||
log(LogLevelValues.debug, `http call start, cluster: ${this.cluster.clusterName()}, method: ${method}, path: ${path}, body: ${String.UTF8.decode(body)}, timeout: ${timeoutMillisecond}`)
|
||||
if (result != WasmResultValues.Ok) {
|
||||
log(LogLevelValues.error, `http call failed, result: ${result}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("GET", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("HEAD", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("OPTIONS", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("POST", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("PUT", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("PATCH", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("DELETE", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("CONNECT", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("TRACE", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
}
|
||||
18
plugins/wasm-assemblyscript/assembly/index.ts
Normal file
18
plugins/wasm-assemblyscript/assembly/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export {RouteCluster,
|
||||
K8sCluster,
|
||||
NacosCluster,
|
||||
ConsulCluster,
|
||||
FQDNCluster,
|
||||
StaticIpCluster} from "./cluster_wrapper"
|
||||
export {HttpClient,
|
||||
ClusterClient} from "./http_wrapper"
|
||||
export {Log} from "./log_wrapper"
|
||||
export {SetCtx,
|
||||
HttpContext,
|
||||
ParseConfigBy,
|
||||
ProcessRequestBodyBy,
|
||||
ProcessRequestHeadersBy,
|
||||
ProcessResponseBodyBy,
|
||||
ProcessResponseHeadersBy,
|
||||
Logger, RegisteTickFunc} from "./plugin_wrapper"
|
||||
export {ParseResult} from "./rule_matcher"
|
||||
66
plugins/wasm-assemblyscript/assembly/log_wrapper.ts
Normal file
66
plugins/wasm-assemblyscript/assembly/log_wrapper.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { log, LogLevelValues } from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
|
||||
enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Critical,
|
||||
}
|
||||
|
||||
export class Log {
|
||||
private pluginName: string;
|
||||
|
||||
constructor(pluginName: string) {
|
||||
this.pluginName = pluginName;
|
||||
}
|
||||
|
||||
private log(level: LogLevel, msg: string): void {
|
||||
let formattedMsg = `[${this.pluginName}] ${msg}`;
|
||||
switch (level) {
|
||||
case LogLevel.Trace:
|
||||
log(LogLevelValues.trace, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
log(LogLevelValues.debug, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
log(LogLevelValues.info, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
log(LogLevelValues.warn, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
log(LogLevelValues.error, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
log(LogLevelValues.critical, formattedMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Trace(msg: string): void {
|
||||
this.log(LogLevel.Trace, msg);
|
||||
}
|
||||
|
||||
public Debug(msg: string): void {
|
||||
this.log(LogLevel.Debug, msg);
|
||||
}
|
||||
|
||||
public Info(msg: string): void {
|
||||
this.log(LogLevel.Info, msg);
|
||||
}
|
||||
|
||||
public Warn(msg: string): void {
|
||||
this.log(LogLevel.Warn, msg);
|
||||
}
|
||||
|
||||
public Error(msg: string): void {
|
||||
this.log(LogLevel.Error, msg);
|
||||
}
|
||||
|
||||
public Critical(msg: string): void {
|
||||
this.log(LogLevel.Critical, msg);
|
||||
}
|
||||
}
|
||||
445
plugins/wasm-assemblyscript/assembly/plugin_wrapper.ts
Normal file
445
plugins/wasm-assemblyscript/assembly/plugin_wrapper.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
import { Log } from "./log_wrapper";
|
||||
import {
|
||||
Context,
|
||||
FilterHeadersStatusValues,
|
||||
RootContext,
|
||||
setRootContext,
|
||||
proxy_set_effective_context,
|
||||
log,
|
||||
LogLevelValues,
|
||||
FilterDataStatusValues,
|
||||
get_buffer_bytes,
|
||||
BufferTypeValues,
|
||||
set_tick_period_milliseconds,
|
||||
get_current_time_nanoseconds
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
import {
|
||||
getRequestHost,
|
||||
getRequestMethod,
|
||||
getRequestPath,
|
||||
getRequestScheme,
|
||||
isBinaryRequestBody,
|
||||
} from "./request_wrapper";
|
||||
import { RuleMatcher, ParseResult } from "./rule_matcher";
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
|
||||
export function SetCtx<PluginConfig>(
|
||||
pluginName: string,
|
||||
setFuncs: usize[] = []
|
||||
): void {
|
||||
const rootContextId = 1
|
||||
setRootContext(new CommonRootCtx<PluginConfig>(rootContextId, pluginName, setFuncs));
|
||||
}
|
||||
|
||||
export interface HttpContext {
|
||||
Scheme(): string;
|
||||
Host(): string;
|
||||
Path(): string;
|
||||
Method(): string;
|
||||
SetContext(key: string, value: usize): void;
|
||||
GetContext(key: string): usize;
|
||||
DontReadRequestBody(): void;
|
||||
DontReadResponseBody(): void;
|
||||
}
|
||||
|
||||
type ParseConfigFunc<PluginConfig> = (
|
||||
json: JSON.Obj,
|
||||
) => ParseResult<PluginConfig>;
|
||||
type OnHttpHeadersFunc<PluginConfig> = (
|
||||
context: HttpContext,
|
||||
config: PluginConfig,
|
||||
) => FilterHeadersStatusValues;
|
||||
type OnHttpBodyFunc<PluginConfig> = (
|
||||
context: HttpContext,
|
||||
config: PluginConfig,
|
||||
body: ArrayBuffer,
|
||||
) => FilterDataStatusValues;
|
||||
|
||||
|
||||
export var Logger: Log = new Log("");
|
||||
|
||||
class CommonRootCtx<PluginConfig> extends RootContext {
|
||||
pluginName: string;
|
||||
hasCustomConfig: boolean;
|
||||
ruleMatcher: RuleMatcher<PluginConfig>;
|
||||
parseConfig: ParseConfigFunc<PluginConfig> | null;
|
||||
onHttpRequestHeaders: OnHttpHeadersFunc<PluginConfig> | null;
|
||||
onHttpRequestBody: OnHttpBodyFunc<PluginConfig> | null;
|
||||
onHttpResponseHeaders: OnHttpHeadersFunc<PluginConfig> | null;
|
||||
onHttpResponseBody: OnHttpBodyFunc<PluginConfig> | null;
|
||||
onTickFuncs: Array<TickFuncEntry>;
|
||||
|
||||
constructor(context_id: u32, pluginName: string, setFuncs: usize[]) {
|
||||
super(context_id);
|
||||
this.pluginName = pluginName;
|
||||
Logger = new Log(pluginName);
|
||||
this.hasCustomConfig = true;
|
||||
this.onHttpRequestHeaders = null;
|
||||
this.onHttpRequestBody = null;
|
||||
this.onHttpResponseHeaders = null;
|
||||
this.onHttpResponseBody = null;
|
||||
this.parseConfig = null;
|
||||
this.ruleMatcher = new RuleMatcher<PluginConfig>();
|
||||
this.onTickFuncs = new Array<TickFuncEntry>();
|
||||
for (let i = 0; i < setFuncs.length; i++) {
|
||||
changetype<Closure<PluginConfig>>(setFuncs[i]).lambdaFn(
|
||||
setFuncs[i],
|
||||
this
|
||||
);
|
||||
}
|
||||
if (this.parseConfig == null) {
|
||||
this.hasCustomConfig = false;
|
||||
this.parseConfig = (json: JSON.Obj): ParseResult<PluginConfig> =>{ return new ParseResult<PluginConfig>(null, true); };
|
||||
}
|
||||
}
|
||||
|
||||
createContext(context_id: u32): Context {
|
||||
return new CommonCtx<PluginConfig>(context_id, this);
|
||||
}
|
||||
|
||||
onConfigure(configuration_size: u32): boolean {
|
||||
super.onConfigure(configuration_size);
|
||||
const data = this.getConfiguration();
|
||||
let jsonData: JSON.Obj = new JSON.Obj();
|
||||
if (data == "{}") {
|
||||
if (this.hasCustomConfig) {
|
||||
log(LogLevelValues.warn, "config is empty, but has ParseConfigFunc");
|
||||
}
|
||||
} else {
|
||||
const parseData = JSON.parse(data);
|
||||
if (parseData.isObj) {
|
||||
jsonData = changetype<JSON.Obj>(JSON.parse(data));
|
||||
} else {
|
||||
log(LogLevelValues.error, "parse json data failed")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.ruleMatcher.parseRuleConfig(jsonData, this.parseConfig as ParseConfigFunc<PluginConfig>)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (globalOnTickFuncs.length > 0) {
|
||||
this.onTickFuncs = globalOnTickFuncs;
|
||||
set_tick_period_milliseconds(100);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onTick(): void {
|
||||
for (let i = 0; i < this.onTickFuncs.length; i++) {
|
||||
const tickFuncEntry = this.onTickFuncs[i];
|
||||
const now = getCurrentTimeMilliseconds();
|
||||
if (tickFuncEntry.lastExecuted + tickFuncEntry.tickPeriod <= now) {
|
||||
tickFuncEntry.tickFunc();
|
||||
tickFuncEntry.lastExecuted = getCurrentTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTimeMilliseconds(): u64 {
|
||||
return get_current_time_nanoseconds() / 1000000;
|
||||
}
|
||||
|
||||
class TickFuncEntry {
|
||||
lastExecuted: u64;
|
||||
tickPeriod: u64;
|
||||
tickFunc: () => void;
|
||||
|
||||
constructor(lastExecuted: u64, tickPeriod: u64, tickFunc: () => void) {
|
||||
this.lastExecuted = lastExecuted;
|
||||
this.tickPeriod = tickPeriod;
|
||||
this.tickFunc = tickFunc;
|
||||
}
|
||||
}
|
||||
|
||||
var globalOnTickFuncs = new Array<TickFuncEntry>();
|
||||
|
||||
export function RegisteTickFunc(tickPeriod: i64, tickFunc: () => void): void {
|
||||
globalOnTickFuncs.push(new TickFuncEntry(0, tickPeriod, tickFunc));
|
||||
}
|
||||
|
||||
class Closure<PluginConfig> {
|
||||
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void;
|
||||
parseConfigFunc: ParseConfigFunc<PluginConfig> | null;
|
||||
onHttpHeadersFunc: OnHttpHeadersFunc<PluginConfig> | null;
|
||||
OnHttpBodyFunc: OnHttpBodyFunc<PluginConfig> | null;
|
||||
|
||||
constructor(
|
||||
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void
|
||||
) {
|
||||
this.lambdaFn = lambdaFn;
|
||||
this.parseConfigFunc = null;
|
||||
this.onHttpHeadersFunc = null;
|
||||
this.OnHttpBodyFunc = null;
|
||||
}
|
||||
|
||||
setParseConfigFunc(f: ParseConfigFunc<PluginConfig>): void {
|
||||
this.parseConfigFunc = f;
|
||||
}
|
||||
|
||||
setHttpHeadersFunc(f: OnHttpHeadersFunc<PluginConfig>): void {
|
||||
this.onHttpHeadersFunc = f;
|
||||
}
|
||||
|
||||
setHttpBodyFunc(f: OnHttpBodyFunc<PluginConfig>): void {
|
||||
this.OnHttpBodyFunc = f;
|
||||
}
|
||||
}
|
||||
|
||||
export function ParseConfigBy<PluginConfig>(
|
||||
f: ParseConfigFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).parseConfigFunc;
|
||||
if (f != null) {
|
||||
ctx.parseConfig = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setParseConfigFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessRequestHeadersBy<PluginConfig>(
|
||||
f: OnHttpHeadersFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpRequestHeaders = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpHeadersFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessRequestBodyBy<PluginConfig>(
|
||||
f: OnHttpBodyFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpRequestBody = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpBodyFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessResponseHeadersBy<PluginConfig>(
|
||||
f: OnHttpHeadersFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpResponseHeaders = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpHeadersFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessResponseBodyBy<PluginConfig>(
|
||||
f: OnHttpBodyFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpResponseBody = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpBodyFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
class CommonCtx<PluginConfig> extends Context implements HttpContext {
|
||||
commonRootCtx: CommonRootCtx<PluginConfig>;
|
||||
config: PluginConfig |null;
|
||||
needRequestBody: boolean;
|
||||
needResponseBody: boolean;
|
||||
requestBodySize: u32;
|
||||
responseBodySize: u32;
|
||||
contextID: u32;
|
||||
userContext: Map<string, usize>;
|
||||
|
||||
constructor(context_id: u32, root_context: CommonRootCtx<PluginConfig>) {
|
||||
super(context_id, root_context);
|
||||
this.userContext = new Map<string, usize>();
|
||||
this.commonRootCtx = root_context;
|
||||
this.contextID = context_id;
|
||||
this.requestBodySize = 0;
|
||||
this.responseBodySize = 0;
|
||||
this.config = null
|
||||
if (this.commonRootCtx.onHttpRequestHeaders != null) {
|
||||
this.needResponseBody = true;
|
||||
} else {
|
||||
this.needResponseBody = false;
|
||||
}
|
||||
if (this.commonRootCtx.onHttpRequestBody != null) {
|
||||
this.needRequestBody = true;
|
||||
} else {
|
||||
this.needRequestBody = false;
|
||||
}
|
||||
}
|
||||
|
||||
SetContext(key: string, value: usize): void {
|
||||
this.userContext.set(key, value);
|
||||
}
|
||||
|
||||
GetContext(key: string): usize {
|
||||
return this.userContext.get(key);
|
||||
}
|
||||
|
||||
Scheme(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestScheme();
|
||||
}
|
||||
|
||||
Host(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestHost();
|
||||
}
|
||||
|
||||
Path(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestPath();
|
||||
}
|
||||
|
||||
Method(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestMethod();
|
||||
}
|
||||
|
||||
DontReadRequestBody(): void {
|
||||
this.needRequestBody = false;
|
||||
}
|
||||
|
||||
DontReadResponseBody(): void {
|
||||
this.needResponseBody = false;
|
||||
}
|
||||
|
||||
onRequestHeaders(_a: u32, _end_of_stream: boolean): FilterHeadersStatusValues {
|
||||
const parseResult = this.commonRootCtx.ruleMatcher.getMatchConfig();
|
||||
if (parseResult.success == false) {
|
||||
log(LogLevelValues.error, "get match config failed");
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
this.config = parseResult.pluginConfig;
|
||||
|
||||
if (isBinaryRequestBody()) {
|
||||
this.needRequestBody = false;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpRequestHeaders == null) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
return this.commonRootCtx.onHttpRequestHeaders(
|
||||
this,
|
||||
this.config as PluginConfig
|
||||
);
|
||||
}
|
||||
|
||||
onRequestBody(
|
||||
body_buffer_length: usize,
|
||||
end_of_stream: boolean
|
||||
): FilterDataStatusValues {
|
||||
if (this.config == null || !this.needRequestBody) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpRequestBody == null) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
this.requestBodySize += body_buffer_length as u32;
|
||||
|
||||
if (!end_of_stream) {
|
||||
return FilterDataStatusValues.StopIterationAndBuffer;
|
||||
}
|
||||
|
||||
const body = get_buffer_bytes(
|
||||
BufferTypeValues.HttpRequestBody,
|
||||
0,
|
||||
this.requestBodySize
|
||||
);
|
||||
|
||||
return this.commonRootCtx.onHttpRequestBody(
|
||||
this,
|
||||
this.config as PluginConfig,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
onResponseHeaders(_a: u32, _end_of_stream: bool): FilterHeadersStatusValues {
|
||||
if (this.config == null) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (isBinaryRequestBody()) {
|
||||
this.needResponseBody = false;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpResponseHeaders == null) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
|
||||
return this.commonRootCtx.onHttpResponseHeaders(
|
||||
this,
|
||||
this.config as PluginConfig
|
||||
);
|
||||
}
|
||||
|
||||
onResponseBody(
|
||||
body_buffer_length: usize,
|
||||
end_of_stream: bool
|
||||
): FilterDataStatusValues {
|
||||
if (this.config == null) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpResponseBody == null) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (!this.needResponseBody) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
this.responseBodySize += body_buffer_length as u32;
|
||||
|
||||
if (!end_of_stream) {
|
||||
return FilterDataStatusValues.StopIterationAndBuffer;
|
||||
}
|
||||
const body = get_buffer_bytes(
|
||||
BufferTypeValues.HttpResponseBody,
|
||||
0,
|
||||
this.responseBodySize
|
||||
);
|
||||
|
||||
return this.commonRootCtx.onHttpResponseBody(
|
||||
this,
|
||||
this.config as PluginConfig,
|
||||
body
|
||||
);
|
||||
}
|
||||
}
|
||||
65
plugins/wasm-assemblyscript/assembly/request_wrapper.ts
Normal file
65
plugins/wasm-assemblyscript/assembly/request_wrapper.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
stream_context,
|
||||
log,
|
||||
LogLevelValues
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
|
||||
export function getRequestScheme(): string {
|
||||
let scheme: string = stream_context.headers.request.get(":scheme");
|
||||
if (scheme == "") {
|
||||
log(LogLevelValues.error, "Parse request scheme failed");
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
export function getRequestHost(): string {
|
||||
let host: string = stream_context.headers.request.get(":authority");
|
||||
if (host == "") {
|
||||
log(LogLevelValues.error, "Parse request host failed");
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
export function getRequestPath(): string {
|
||||
let path: string = stream_context.headers.request.get(":path");
|
||||
if (path == "") {
|
||||
log(LogLevelValues.error, "Parse request path failed");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function getRequestMethod(): string {
|
||||
let method: string = stream_context.headers.request.get(":method");
|
||||
if (method == "") {
|
||||
log(LogLevelValues.error, "Parse request method failed");
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
export function isBinaryRequestBody(): boolean {
|
||||
let contentType: string = stream_context.headers.request.get("content-type");
|
||||
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let encoding: string = stream_context.headers.request.get("content-encoding");
|
||||
if (encoding != "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isBinaryResponseBody(): boolean {
|
||||
let contentType: string = stream_context.headers.response.get("content-type");
|
||||
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let encoding: string = stream_context.headers.response.get("content-encoding");
|
||||
if (encoding != "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
346
plugins/wasm-assemblyscript/assembly/rule_matcher.ts
Normal file
346
plugins/wasm-assemblyscript/assembly/rule_matcher.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { getRequestHost } from "./request_wrapper";
|
||||
import {
|
||||
get_property,
|
||||
LogLevelValues,
|
||||
log,
|
||||
WasmResultValues,
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
|
||||
enum Category {
|
||||
Route,
|
||||
Host,
|
||||
RoutePrefix,
|
||||
Service
|
||||
}
|
||||
|
||||
enum MatchType {
|
||||
Prefix,
|
||||
Exact,
|
||||
Suffix,
|
||||
}
|
||||
|
||||
const RULES_KEY: string = "_rules_";
|
||||
const MATCH_ROUTE_KEY: string = "_match_route_";
|
||||
const MATCH_DOMAIN_KEY: string = "_match_domain_";
|
||||
const MATCH_SERVICE_KEY: string = "_match_service_";
|
||||
const MATCH_ROUTE_PREFIX_KEY: string = "_match_route_prefix_"
|
||||
|
||||
class HostMatcher {
|
||||
matchType: MatchType;
|
||||
host: string;
|
||||
|
||||
constructor(matchType: MatchType, host: string) {
|
||||
this.matchType = matchType;
|
||||
this.host = host;
|
||||
}
|
||||
}
|
||||
|
||||
class RuleConfig<PluginConfig> {
|
||||
category: Category;
|
||||
routes!: Map<string, boolean>;
|
||||
services!: Map<string, boolean>;
|
||||
routePrefixs!: Map<string, boolean>;
|
||||
hosts!: Array<HostMatcher>;
|
||||
config: PluginConfig | null;
|
||||
|
||||
constructor() {
|
||||
this.category = Category.Route;
|
||||
this.config = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseResult<PluginConfig> {
|
||||
pluginConfig: PluginConfig | null;
|
||||
success: boolean;
|
||||
constructor(pluginConfig: PluginConfig | null, success: boolean) {
|
||||
this.pluginConfig = pluginConfig;
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
|
||||
export class RuleMatcher<PluginConfig> {
|
||||
ruleConfig: Array<RuleConfig<PluginConfig>>;
|
||||
globalConfig: PluginConfig | null;
|
||||
hasGlobalConfig: boolean;
|
||||
|
||||
constructor() {
|
||||
this.ruleConfig = new Array<RuleConfig<PluginConfig>>();
|
||||
this.globalConfig = null;
|
||||
this.hasGlobalConfig = false;
|
||||
}
|
||||
|
||||
getMatchConfig(): ParseResult<PluginConfig> {
|
||||
const host = getRequestHost();
|
||||
if (host == "") {
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
let result = get_property("route_name");
|
||||
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
const routeName = String.UTF8.decode(result.returnValue);
|
||||
|
||||
result = get_property("cluster_name");
|
||||
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
const serviceName = String.UTF8.decode(result.returnValue);
|
||||
|
||||
for (let i = 0; i < this.ruleConfig.length; i++) {
|
||||
const rule = this.ruleConfig[i];
|
||||
// category == Host
|
||||
if (rule.category == Category.Host) {
|
||||
if (this.hostMatch(rule, host)) {
|
||||
log(LogLevelValues.debug, "getMatchConfig: match host " + host);
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if (rule.category == Category.Route) {
|
||||
if (rule.routes.has(routeName)) {
|
||||
log(LogLevelValues.debug, "getMatchConfig: match route " + routeName);
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
// category == RoutePrefix
|
||||
if (rule.category == Category.RoutePrefix) {
|
||||
for (let i = 0; i < rule.routePrefixs.keys().length; i++) {
|
||||
const routePrefix = rule.routePrefixs.keys()[i];
|
||||
if (routeName.startsWith(routePrefix)) {
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// category == Cluster
|
||||
if (this.serviceMatch(rule, serviceName)) {
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasGlobalConfig) {
|
||||
return new ParseResult<PluginConfig>(this.globalConfig, true);
|
||||
}
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
|
||||
parseRuleConfig(
|
||||
config: JSON.Obj,
|
||||
parsePluginConfig: (json: JSON.Obj) => ParseResult<PluginConfig>
|
||||
): boolean {
|
||||
const obj = config;
|
||||
let keyCount = obj.keys.length;
|
||||
if (keyCount == 0) {
|
||||
this.hasGlobalConfig = true;
|
||||
const parseResult = parsePluginConfig(config);
|
||||
if (parseResult.success) {
|
||||
this.globalConfig = parseResult.pluginConfig;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let rules: JSON.Arr | null = null;
|
||||
if (obj.has(RULES_KEY)) {
|
||||
rules = obj.getArr(RULES_KEY);
|
||||
keyCount--;
|
||||
}
|
||||
|
||||
if (keyCount > 0) {
|
||||
const parseResult = parsePluginConfig(config);
|
||||
if (parseResult.success) {
|
||||
this.globalConfig = parseResult.pluginConfig;
|
||||
this.hasGlobalConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rules) {
|
||||
if (this.hasGlobalConfig) {
|
||||
return true;
|
||||
}
|
||||
log(LogLevelValues.error, "parse config failed, no valid rules; global config parse error");
|
||||
return false;
|
||||
}
|
||||
|
||||
const rulesArray = rules.valueOf();
|
||||
for (let i = 0; i < rulesArray.length; i++) {
|
||||
if (!rulesArray[i].isObj) {
|
||||
log(LogLevelValues.error, "parse rule failed, rules must be an array of objects");
|
||||
continue;
|
||||
}
|
||||
const ruleJson = changetype<JSON.Obj>(rulesArray[i]);
|
||||
const rule = new RuleConfig<PluginConfig>();
|
||||
const parseResult = parsePluginConfig(ruleJson);
|
||||
if (parseResult.success) {
|
||||
rule.config = parseResult.pluginConfig;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
rule.routes = this.parseRouteMatchConfig(ruleJson);
|
||||
rule.hosts = this.parseHostMatchConfig(ruleJson);
|
||||
rule.services = this.parseServiceMatchConfig(ruleJson);
|
||||
rule.routePrefixs = this.parseRoutePrefixMatchConfig(ruleJson);
|
||||
|
||||
const noRoute = rule.routes.size == 0;
|
||||
const noHosts = rule.hosts.length == 0;
|
||||
const noServices = rule.services.size == 0;
|
||||
const noRoutePrefixs = rule.routePrefixs.size == 0;
|
||||
|
||||
if ((boolToInt(noRoute) + boolToInt(noHosts) + boolToInt(noServices) + boolToInt(noRoutePrefixs)) != 3) {
|
||||
log(LogLevelValues.error, "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.");
|
||||
return false;
|
||||
}
|
||||
if (!noRoute) {
|
||||
rule.category = Category.Route;
|
||||
} else if (!noHosts) {
|
||||
rule.category = Category.Host;
|
||||
} else if (!noServices) {
|
||||
rule.category = Category.Service;
|
||||
} else {
|
||||
rule.category = Category.RoutePrefix;
|
||||
}
|
||||
this.ruleConfig.push(rule);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
parseRouteMatchConfig(config: JSON.Obj): Map<string, boolean> {
|
||||
const keys = config.getArr(MATCH_ROUTE_KEY);
|
||||
const routes = new Map<string, boolean>();
|
||||
if (keys) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const key = array[i].toString();
|
||||
if (key != "") {
|
||||
routes.set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
parseRoutePrefixMatchConfig(config: JSON.Obj): Map<string, boolean> {
|
||||
const keys = config.getArr(MATCH_ROUTE_PREFIX_KEY);
|
||||
const routePrefixs = new Map<string, boolean>();
|
||||
if (keys) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const key = array[i].toString();
|
||||
if (key != "") {
|
||||
routePrefixs.set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return routePrefixs;
|
||||
}
|
||||
|
||||
parseServiceMatchConfig(config: JSON.Obj): Map<string, boolean> {
|
||||
const keys = config.getArr(MATCH_SERVICE_KEY);
|
||||
const clusters = new Map<string, boolean>();
|
||||
if (keys) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const key = array[i].toString();
|
||||
if (key != "") {
|
||||
clusters.set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
|
||||
parseHostMatchConfig(config: JSON.Obj): Array<HostMatcher> {
|
||||
const hostMatchers = new Array<HostMatcher>();
|
||||
const keys = config.getArr(MATCH_DOMAIN_KEY);
|
||||
if (keys !== null) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i].toString(); // Assuming the array has string elements
|
||||
let hostMatcher: HostMatcher;
|
||||
if (item.startsWith("*")) {
|
||||
hostMatcher = new HostMatcher(MatchType.Suffix, item.substr(1));
|
||||
} else if (item.endsWith("*")) {
|
||||
hostMatcher = new HostMatcher(
|
||||
MatchType.Prefix,
|
||||
item.substr(0, item.length - 1)
|
||||
);
|
||||
} else {
|
||||
hostMatcher = new HostMatcher(MatchType.Exact, item);
|
||||
}
|
||||
hostMatchers.push(hostMatcher);
|
||||
}
|
||||
}
|
||||
return hostMatchers;
|
||||
}
|
||||
|
||||
stripPortFromHost(reqHost: string): string {
|
||||
// Port removing code is inspired by
|
||||
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
|
||||
let portStart: i32 = reqHost.lastIndexOf(":");
|
||||
if (portStart != -1) {
|
||||
// According to RFC3986 v6 address is always enclosed in "[]".
|
||||
// section 3.2.2.
|
||||
let v6EndIndex: i32 = reqHost.lastIndexOf("]");
|
||||
if (v6EndIndex == -1 || v6EndIndex < portStart) {
|
||||
if (portStart + 1 <= reqHost.length) {
|
||||
return reqHost.substring(0, portStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reqHost;
|
||||
}
|
||||
|
||||
hostMatch(rule: RuleConfig<PluginConfig>, reqHost: string): boolean {
|
||||
reqHost = this.stripPortFromHost(reqHost);
|
||||
for (let i = 0; i < rule.hosts.length; i++) {
|
||||
let hostMatch = rule.hosts[i];
|
||||
switch (hostMatch.matchType) {
|
||||
case MatchType.Suffix:
|
||||
if (reqHost.endsWith(hostMatch.host)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MatchType.Prefix:
|
||||
if (reqHost.startsWith(hostMatch.host)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MatchType.Exact:
|
||||
if (reqHost == hostMatch.host) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
serviceMatch(rule: RuleConfig<PluginConfig>, serviceName: string): boolean {
|
||||
const parts = serviceName.split('|');
|
||||
if (parts.length != 4) {
|
||||
return false;
|
||||
}
|
||||
const port = parts[1];
|
||||
const fqdn = parts[3];
|
||||
for (let i = 0; i < rule.services.keys().length; i++) {
|
||||
let configServiceName = rule.services.keys()[i];
|
||||
let colonIndex = configServiceName.lastIndexOf(':');
|
||||
if (colonIndex != -1) {
|
||||
let configFQDN = configServiceName.slice(0, colonIndex);
|
||||
let configPort = configServiceName.slice(colonIndex + 1);
|
||||
if (fqdn == configFQDN && port == configPort) return true;
|
||||
} else if (fqdn == configServiceName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function boolToInt(value: boolean): i32 {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
6
plugins/wasm-assemblyscript/assembly/tsconfig.json
Normal file
6
plugins/wasm-assemblyscript/assembly/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 功能说明
|
||||
`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。
|
||||
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| headers | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||
|
||||
# 配置示例
|
||||
|
||||
## Mock 应答场景
|
||||
|
||||
```yaml
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
|
||||
```
|
||||
|
||||
根据该配置,请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Hello: World
|
||||
Content-Length: 17
|
||||
|
||||
{"hello":"world"}
|
||||
```
|
||||
|
||||
## 触发限流时自定义响应
|
||||
|
||||
```yaml
|
||||
enable_on_status:
|
||||
- 429
|
||||
status_code: 302
|
||||
headers:
|
||||
- Location=https://example.com
|
||||
```
|
||||
|
||||
触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 302 Found
|
||||
Location: https://example.com
|
||||
```
|
||||
|
||||
从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。
|
||||
|
||||
如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。
|
||||
|
||||
## 对特定路由或域名开启
|
||||
```yaml
|
||||
# 使用 matchRules 字段进行细粒度规则配置
|
||||
matchRules:
|
||||
# 规则一:按 Ingress 名称匹配生效
|
||||
- ingress:
|
||||
- default/foo
|
||||
- default/bar
|
||||
body: "{\"hello\":\"world\"}"
|
||||
# 规则二:按域名匹配生效
|
||||
- domain:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
enable_on_status:
|
||||
- 429
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
body: "{\"errmsg\": \"rate limited\"}"
|
||||
```
|
||||
此例 `ingress` 中指定的 `default/foo` 和 `default/bar` 对应 default 命名空间下名为 foo 和 bar 的 Ingress,当匹配到这两个 Ingress 时,将使用此段配置;
|
||||
此例 `domain` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
|
||||
配置的匹配生效顺序,将按照 `matchRules` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "build/debug.wasm",
|
||||
"textFile": "build/debug.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false,
|
||||
"debug": true
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"bindings": "esm",
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
|
||||
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseConfigBy, ParseResult, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
|
||||
import { FilterHeadersStatusValues, Headers, send_http_response, stream_context, HeaderPair } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
|
||||
class CustomResponseConfig {
|
||||
statusCode: u32;
|
||||
headers: Headers;
|
||||
body: ArrayBuffer;
|
||||
enableOnStatus: Array<u32>;
|
||||
contentType: string;
|
||||
constructor() {
|
||||
this.statusCode = 200;
|
||||
this.headers = [];
|
||||
this.body = new ArrayBuffer(0);
|
||||
this.enableOnStatus = [];
|
||||
this.contentType = "text/plain; charset=utf-8";
|
||||
}
|
||||
}
|
||||
|
||||
SetCtx<CustomResponseConfig>(
|
||||
"custom-response",
|
||||
[ParseConfigBy<CustomResponseConfig>(parseConfig),
|
||||
ProcessRequestHeadersBy<CustomResponseConfig>(onHttpRequestHeaders),
|
||||
ProcessResponseHeadersBy<CustomResponseConfig>(onHttpResponseHeaders),])
|
||||
|
||||
function parseConfig(json: JSON.Obj): ParseResult<CustomResponseConfig> {
|
||||
let headersArray = json.getArr("headers");
|
||||
let config = new CustomResponseConfig();
|
||||
if (headersArray != null) {
|
||||
for (let i = 0; i < headersArray.valueOf().length; i++) {
|
||||
let header = headersArray._arr[i];
|
||||
let jsonString = (<JSON.Str>header).toString()
|
||||
let kv = jsonString.split("=")
|
||||
if (kv.length == 2) {
|
||||
let key = kv[0].trim();
|
||||
let value = kv[1].trim();
|
||||
if (key.toLowerCase() == "content-type") {
|
||||
config.contentType = value;
|
||||
} else if (key.toLowerCase() == "content-length") {
|
||||
continue;
|
||||
} else {
|
||||
config.headers.push(new HeaderPair(String.UTF8.encode(key), String.UTF8.encode(value)));
|
||||
}
|
||||
} else {
|
||||
Logger.Error("parse header failed");
|
||||
return new ParseResult<CustomResponseConfig>(null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = json.getString("body");
|
||||
if (body != null) {
|
||||
config.body = String.UTF8.encode(body.valueOf());
|
||||
}
|
||||
config.headers.push(new HeaderPair(String.UTF8.encode("content-type"), String.UTF8.encode(config.contentType)));
|
||||
|
||||
let statusCode = json.getInteger("statusCode");
|
||||
if (statusCode != null) {
|
||||
config.statusCode = statusCode.valueOf() as u32;
|
||||
}
|
||||
|
||||
let enableOnStatus = json.getArr("enableOnStatus");
|
||||
|
||||
if (enableOnStatus != null) {
|
||||
for (let i = 0; i < enableOnStatus.valueOf().length; i++) {
|
||||
let status = enableOnStatus._arr[i];
|
||||
if (status.isInteger) {
|
||||
config.enableOnStatus.push((<JSON.Integer>status).valueOf() as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ParseResult<CustomResponseConfig>(config, true);
|
||||
}
|
||||
|
||||
function onHttpRequestHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
|
||||
if (config.enableOnStatus.length != 0) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
|
||||
return FilterHeadersStatusValues.StopIteration;
|
||||
}
|
||||
|
||||
function onHttpResponseHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
|
||||
let statusCodeStr = stream_context.headers.response.get(":status")
|
||||
if (statusCodeStr == "") {
|
||||
Logger.Error("get http response status code failed");
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
let statusCode = parseInt(statusCodeStr);
|
||||
for (let i = 0; i < config.enableOnStatus.length; i++) {
|
||||
if (statusCode == config.enableOnStatus[i]) {
|
||||
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
|
||||
}
|
||||
}
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
68
plugins/wasm-assemblyscript/extensions/custom-response/package-lock.json
generated
Normal file
68
plugins/wasm-assemblyscript/extensions/custom-response/package-lock.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "custom-response",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "custom-response",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@higress/wasm-assemblyscript": "^0.0.4",
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@higress/wasm-assemblyscript": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.4.tgz",
|
||||
"integrity": "sha512-F9m3fHBeM0OFWWHekTcmj3dVh7I4pbzf0oIioVdArD2oSUgpCZ8ur8E/9r7JR3WVwn2/l0A3LRSBOJTzQnHtMw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.29",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
|
||||
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript-json": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
|
||||
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "custom-response",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node tests",
|
||||
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||
"asbuild:release": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||
"start": "npx serve ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0",
|
||||
"@higress/wasm-assemblyscript": "^0.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./build/release.js",
|
||||
"types": "./build/release.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "build/debug.wasm",
|
||||
"textFile": "build/debug.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false,
|
||||
"debug": true
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"bindings": "esm",
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
|
||||
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseResult, ParseConfigBy, RegisteTickFunc, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
|
||||
import { FilterHeadersStatusValues, send_http_response, stream_context } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
class HelloWorldConfig {
|
||||
}
|
||||
|
||||
SetCtx<HelloWorldConfig>("hello-world",
|
||||
[ParseConfigBy<HelloWorldConfig>(parseConfig),
|
||||
ProcessRequestHeadersBy<HelloWorldConfig>(onHttpRequestHeaders),
|
||||
ProcessResponseHeadersBy<HelloWorldConfig>(onHttpResponseHeaders)
|
||||
])
|
||||
|
||||
function parseConfig(json: JSON.Obj): ParseResult<HelloWorldConfig> {
|
||||
RegisteTickFunc(2000, () => {
|
||||
Logger.Debug("tick 2s");
|
||||
})
|
||||
RegisteTickFunc(5000, () => {
|
||||
Logger.Debug("tick 5s");
|
||||
})
|
||||
return new ParseResult<HelloWorldConfig>(new HelloWorldConfig(), true);
|
||||
}
|
||||
|
||||
class TestContext{
|
||||
value: string
|
||||
constructor(value: string){
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
function onHttpRequestHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
|
||||
stream_context.headers.request.add("hello", "world");
|
||||
Logger.Debug("[hello-world] logger test");
|
||||
context.SetContext("test-set-context", changetype<usize>(new TestContext("value")))
|
||||
send_http_response(200, "hello-world", String.UTF8.encode("[wasm-assemblyscript]hello world"), []);
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
|
||||
function onHttpResponseHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
|
||||
const str = changetype<TestContext>(context.GetContext("test-set-context")).value;
|
||||
Logger.Debug("[hello-world] test-set-context: " + str);
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
68
plugins/wasm-assemblyscript/extensions/hello-world/package-lock.json
generated
Normal file
68
plugins/wasm-assemblyscript/extensions/hello-world/package-lock.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@higress/wasm-assemblyscript": "^0.0.4",
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@higress/wasm-assemblyscript": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.4.tgz",
|
||||
"integrity": "sha512-F9m3fHBeM0OFWWHekTcmj3dVh7I4pbzf0oIioVdArD2oSUgpCZ8ur8E/9r7JR3WVwn2/l0A3LRSBOJTzQnHtMw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.29",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
|
||||
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript-json": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
|
||||
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node tests",
|
||||
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||
"asbuild:release": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||
"start": "npx serve ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0",
|
||||
"@higress/wasm-assemblyscript": "^0.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./build/release.js",
|
||||
"types": "./build/release.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
75
plugins/wasm-assemblyscript/package-lock.json
generated
Normal file
75
plugins/wasm-assemblyscript/package-lock.json
generated
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "@higress/wasm-assemblyscript",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@higress/wasm-assemblyscript",
|
||||
"version": "0.0.4",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
|
||||
"as-uuid": "^0.0.4",
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@higress/proxy-wasm-assemblyscript-sdk": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@higress/proxy-wasm-assemblyscript-sdk/-/proxy-wasm-assemblyscript-sdk-0.0.2.tgz",
|
||||
"integrity": "sha512-0J1tFJMTE6o37JpGJBLq0wc5kBC/fpbISrP+KFb4bAEeshu6daXzD2P3bAfJXmW+oZdY0WGptTGXWx8pf9Fk+g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/as-uuid": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/as-uuid/-/as-uuid-0.0.4.tgz",
|
||||
"integrity": "sha512-ZHNv0ETSzg5ZD0IWWJVyip/73LWtrWeMmvRi+16xbkpU/nZ0O8EegvgS7bgZ5xRqrUbc2NqZqHOWMOtPqbLrhg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.29",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
|
||||
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript-json": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
|
||||
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
37
plugins/wasm-assemblyscript/package.json
Normal file
37
plugins/wasm-assemblyscript/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@higress/wasm-assemblyscript",
|
||||
"version": "0.0.4",
|
||||
"main": "assembly/index.ts",
|
||||
"scripts": {
|
||||
"test": "node tests",
|
||||
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||
"asbuild:release": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||
"start": "npx serve ."
|
||||
},
|
||||
"author": "jingze.dai",
|
||||
"license": "Apache-2.0",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29",
|
||||
"as-uuid": "^0.0.4",
|
||||
"assemblyscript-json": "^1.1.0",
|
||||
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./build/release.js",
|
||||
"types": "./build/release.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"/assembly",
|
||||
"package-lock.json",
|
||||
"index.js"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Jing-ze/wasm-assemblyscript.git"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ GO_VERSION ?= 1.19
|
||||
TINYGO_VERSION ?= 0.28.1
|
||||
ORAS_VERSION ?= 1.0.0
|
||||
HIGRESS_VERSION ?= 1.0.0-rc
|
||||
USE_HIGRESS_TINYGO ?= true
|
||||
USE_HIGRESS_TINYGO ?= false
|
||||
BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-tinygo${TINYGO_VERSION}-oras${ORAS_VERSION}
|
||||
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
|
||||
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
|
||||
350
plugins/wasm-go/extensions/ai-agent/README.md
Normal file
350
plugins/wasm-go/extensions/ai-agent/README.md
Normal file
@@ -0,0 +1,350 @@
|
||||
---
|
||||
title: AI Agent
|
||||
keywords: [ AI网关, AI Agent ]
|
||||
description: AI Agent插件配置参考
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
一个可定制化的 API AI Agent,支持配置 http method 类型为 GET 与 POST 的 API,目前只支持非流式模式。
|
||||
agent流程图如下:
|
||||

|
||||
|
||||
|
||||
## 配置字段
|
||||
|
||||
### 基本配置
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|-----------|---------|--------|----------------------------|
|
||||
| `llm` | object | 必填 | - | 配置 AI 服务提供商的信息 |
|
||||
| `apis` | object | 必填 | - | 配置外部 API 服务提供商的信息 |
|
||||
| `promptTemplate` | object | 非必填 | - | 配置 Agent ReAct 模板的信息 |
|
||||
|
||||
`llm`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|-----------|---------|--------|-----------------------------------|
|
||||
| `apiKey` | string | 必填 | - | 用于在访问大模型服务时进行认证的令牌。|
|
||||
| `serviceName` | string | 必填 | - | 大模型服务名 |
|
||||
| `servicePort` | int | 必填 | - | 大模型服务端口 |
|
||||
| `domain` | string | 必填 | - | 访问大模型服务时域名 |
|
||||
| `path` | string | 必填 | - | 访问大模型服务时路径 |
|
||||
| `model` | string | 必填 | - | 访问大模型服务时模型名 |
|
||||
| `maxIterations` | int | 必填 | 15 | 结束执行循环前的最大步数 |
|
||||
| `maxExecutionTime` | int | 必填 | 50000 | 每一次请求大模型的超时时间,单位毫秒 |
|
||||
| `maxTokens` | int | 必填 | 1000 | 每一次请求大模型的输出token限制 |
|
||||
|
||||
`apis`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|---------|--------|-----------------------------------|
|
||||
| `apiProvider` | object | 必填 | - | 外部 API 服务信息 |
|
||||
| `api` | string | 必填 | - | 工具的 OpenAPI 文档 |
|
||||
|
||||
`apiProvider`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|---------|--------|------------------------------------------|
|
||||
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
|
||||
| `serviceName` | string | 必填 | - | 访问外部 API 服务名 |
|
||||
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
|
||||
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
|
||||
|
||||
`apiKey`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|---------|------------|--------|-------------------------------------------------------------------------------|
|
||||
| `in` | string | 非必填 | header | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,默认是 header。
|
||||
| `name` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的名称。 |
|
||||
| `value` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的值。 |
|
||||
|
||||
`promptTemplate`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|-----------|--------|--------------------------------------------|
|
||||
| `language` | string | 非必填 | EN | Agent ReAct 模板的语言类型,包括 CH 和 EN 两种|
|
||||
| `chTemplate` | object | 非必填 | - | Agent ReAct 中文模板 |
|
||||
| `enTemplate` | object | 非必填 | - | Agent ReAct 英文模板 |
|
||||
|
||||
`chTemplate`和`enTemplate`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|-----------|--------|---------------------------------------------|
|
||||
| `question` | string | 非必填 | - | Agent ReAct 模板的 question 部分 |
|
||||
| `thought1` | string | 非必填 | - | Agent ReAct 模板的 thought1 部分 |
|
||||
| `actionInput` | string | 非必填 | - | Agent ReAct 模板的 actionInput 部分 |
|
||||
| `observation` | string | 非必填 | - | Agent ReAct 模板的 observation 部分 |
|
||||
| `thought2` | string | 非必填 | - | Agent ReAct 模板的 thought2 部分 |
|
||||
| `finalAnswer` | string | 非必填 | - | Agent ReAct 模板的 finalAnswer 部分 |
|
||||
| `begin` | string | 非必填 | - | Agent ReAct 模板的 begin 部分 |
|
||||
|
||||
## 用法示例
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
apiKey: xxxxxxxxxxxxxxxxxx
|
||||
domain: dashscope.aliyuncs.com
|
||||
serviceName: dashscope.dns
|
||||
servicePort: 443
|
||||
path: /compatible-mode/v1/chat/completions
|
||||
model: qwen-max-0403
|
||||
maxIterations: 2
|
||||
promptTemplate:
|
||||
language: CH
|
||||
apis:
|
||||
- apiProvider:
|
||||
domain: restapi.amap.com
|
||||
serviceName: geo.dns
|
||||
servicePort: 80
|
||||
apiKey:
|
||||
in: query
|
||||
name: key
|
||||
value: xxxxxxxxxxxxxxx
|
||||
api: |
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: 高德地图
|
||||
description: 获取 POI 的相关信息
|
||||
version: v1.0.0
|
||||
servers:
|
||||
- url: https://restapi.amap.com
|
||||
paths:
|
||||
/v5/place/text:
|
||||
get:
|
||||
description: 根据POI名称,获得POI的经纬度坐标
|
||||
operationId: get_location_coordinate
|
||||
parameters:
|
||||
- name: keywords
|
||||
in: query
|
||||
description: POI名称,必须是中文
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: region
|
||||
in: query
|
||||
description: POI所在的区域名,必须是中文
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
deprecated: false
|
||||
/v5/place/around:
|
||||
get:
|
||||
description: 搜索给定坐标附近的POI
|
||||
operationId: search_nearby_pois
|
||||
parameters:
|
||||
- name: keywords
|
||||
in: query
|
||||
description: 目标POI的关键字
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: location
|
||||
in: query
|
||||
description: 中心点的经度和纬度,用逗号隔开
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
deprecated: false
|
||||
components:
|
||||
schemas: {}
|
||||
- apiProvider:
|
||||
domain: api.seniverse.com
|
||||
serviceName: seniverse.dns
|
||||
servicePort: 80
|
||||
apiKey:
|
||||
in: query
|
||||
name: key
|
||||
value: xxxxxxxxxxxxxxx
|
||||
api: |
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: 心知天气
|
||||
description: 获取 天气预办相关信息
|
||||
version: v1.0.0
|
||||
servers:
|
||||
- url: https://api.seniverse.com
|
||||
paths:
|
||||
/v3/weather/now.json:
|
||||
get:
|
||||
description: 获取指定城市的天气实况
|
||||
operationId: get_weather_now
|
||||
parameters:
|
||||
- name: location
|
||||
in: query
|
||||
description: 所查询的城市
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: language
|
||||
in: query
|
||||
description: 返回天气查询结果所使用的语言
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
default: zh-Hans
|
||||
enum:
|
||||
- zh-Hans
|
||||
- en
|
||||
- ja
|
||||
- name: unit
|
||||
in: query
|
||||
description: 表示温度的的单位,有摄氏度和华氏度两种
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
default: c
|
||||
enum:
|
||||
- c
|
||||
- f
|
||||
deprecated: false
|
||||
components:
|
||||
schemas: {}
|
||||
- apiProvider:
|
||||
apiKey:
|
||||
in: "header"
|
||||
name: "DeepL-Auth-Key"
|
||||
value: "73xxxxxxxxxxxxxxx:fx"
|
||||
domain: "api-free.deepl.com"
|
||||
serviceName: "deepl.dns"
|
||||
servicePort: 443
|
||||
api: |
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: DeepL API Documentation
|
||||
description: The DeepL API provides programmatic access to DeepL’s machine translation technology.
|
||||
version: v1.0.0
|
||||
servers:
|
||||
- url: https://api-free.deepl.com/v2
|
||||
paths:
|
||||
/translate:
|
||||
post:
|
||||
summary: Request Translation
|
||||
operationId: translateText
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
- target_lang
|
||||
properties:
|
||||
text:
|
||||
description: |
|
||||
Text to be translated. Only UTF-8-encoded plain text is supported. The parameter may be specified
|
||||
up to 50 times in a single request. Translations are returned in the same order as they are requested.
|
||||
type: array
|
||||
maxItems: 50
|
||||
items:
|
||||
type: string
|
||||
example: Hello, World!
|
||||
target_lang:
|
||||
description: The language into which the text should be translated.
|
||||
type: string
|
||||
enum:
|
||||
- BG
|
||||
- CS
|
||||
- DA
|
||||
- DE
|
||||
- EL
|
||||
- EN-GB
|
||||
- EN-US
|
||||
- ES
|
||||
- ET
|
||||
- FI
|
||||
- FR
|
||||
- HU
|
||||
- ID
|
||||
- IT
|
||||
- JA
|
||||
- KO
|
||||
- LT
|
||||
- LV
|
||||
- NB
|
||||
- NL
|
||||
- PL
|
||||
- PT-BR
|
||||
- PT-PT
|
||||
- RO
|
||||
- RU
|
||||
- SK
|
||||
- SL
|
||||
- SV
|
||||
- TR
|
||||
- UK
|
||||
- ZH
|
||||
- ZH-HANS
|
||||
example: DE
|
||||
components:
|
||||
schemas: {}
|
||||
```
|
||||
|
||||
本示例配置了三个服务,演示了get与post两种类型的工具。其中get类型的工具包括高德地图与心知天气,post类型的工具是deepl翻译。三个服务都需要现在Higress的服务中以DNS域名的方式配置好,并确保健康。
|
||||
高德地图提供了两个工具,分别是获取指定地点的坐标,以及搜索坐标附近的感兴趣的地点。文档:https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch
|
||||
心知天气提供了一个工具,用于获取指定城市的实时天气情况,支持中文,英文,日语返回,以及摄氏度和华氏度的表示。文档:https://seniverse.yuque.com/hyper_data/api_v3/nyiu3t
|
||||
deepl提供了一个工具,用于翻译给定的句子,支持多语言。。文档:https://developers.deepl.com/docs/v/zh/api-reference/translate?fallback=true
|
||||
|
||||
|
||||
以下为测试用例,为了效果的稳定性,建议保持大模型版本的稳定,本例子中使用的qwen-max-0403:
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"我想在济南市鑫盛大厦附近喝咖啡,给我推荐几个"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"139487e7-96a0-9b13-91b4-290fb79ac992","choices":[{"index":0,"message":{"role":"assistant","content":" 在济南市鑫盛大厦附近,您可以选择以下咖啡店:\n1. luckin coffee 瑞幸咖啡(鑫盛大厦店),位于新泺大街1299号鑫盛大厦2号楼大堂;\n2. 三庆齐盛广场挪瓦咖啡(三庆·齐盛广场店),位于新泺大街与颖秀路交叉口西南60米;\n3. luckin coffee 瑞幸咖啡(三庆·齐盛广场店),位于颖秀路1267号;\n4. 库迪咖啡(齐鲁软件园店),位于新泺大街三庆齐盛广场4号楼底商;\n5. 库迪咖啡(美莲广场店),位于高新区新泺大街1166号美莲广场L117号;以及其他一些选项。希望这些建议对您有所帮助!"},"finish_reason":"stop"}],"created":1723172296,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":886,"completion_tokens":50,"total_tokens":936}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" 济南市现在的天气状况为阴天,温度为31℃。此信息最后更新于2024年8月9日15时12分(北京时间)。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?用华氏度表示,用日语回答"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" 济南市の現在の天気は雨曇りで、気温は88°Fです。この情報は2024年8月9日15時12分(東京時間)に更新されました。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"帮我用德语翻译以下句子:九头蛇万岁!"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"65dcf12c-61ff-9e68-bffa-44fc9e6070d5","choices":[{"index":0,"message":{"role":"assistant","content":" “九头蛇万岁!”的德语翻译为“Hoch lebe Hydra!”。"},"finish_reason":"stop"}],"created":1724043865,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":908,"completion_tokens":52,"total_tokens":960}}
|
||||
```
|
||||
424
plugins/wasm-go/extensions/ai-agent/config.go
Normal file
424
plugins/wasm-go/extensions/ai-agent/config.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty"`
|
||||
PresencePenalty float64 `json:"presence_penalty"`
|
||||
Stream bool `json:"stream"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Topp int32 `json:"top_p"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Object string `json:"object"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
// 用于存放拆解出来的工具相关信息
|
||||
type Tool_Param struct {
|
||||
ToolName string `yaml:"toolName"`
|
||||
Path string `yaml:"path"`
|
||||
Method string `yaml:"method"`
|
||||
ParamName []string `yaml:"paramName"`
|
||||
Parameter string `yaml:"parameter"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// 用于存放拆解出来的api相关信息
|
||||
type APIParam struct {
|
||||
APIKey APIKey `yaml:"apiKey"`
|
||||
URL string `yaml:"url"`
|
||||
Tool_Param []Tool_Param `yaml:"tool_Param"`
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
// 给OpenAPI的get方法用的
|
||||
type Parameter struct {
|
||||
Name string `yaml:"name"`
|
||||
In string `yaml:"in"`
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
Schema struct {
|
||||
Type string `yaml:"type"`
|
||||
Default string `yaml:"default"`
|
||||
Enum []string `yaml:"enum"`
|
||||
} `yaml:"schema"`
|
||||
}
|
||||
|
||||
type Items struct {
|
||||
Type string `yaml:"type"`
|
||||
Example string `yaml:"example"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Description string `yaml:"description"`
|
||||
Type string `yaml:"type"`
|
||||
Enum []string `yaml:"enum,omitempty"`
|
||||
Items *Items `yaml:"items,omitempty"`
|
||||
MaxItems int `yaml:"maxItems,omitempty"`
|
||||
Example string `yaml:"example,omitempty"`
|
||||
}
|
||||
|
||||
type Schema struct {
|
||||
Type string `yaml:"type"`
|
||||
Required []string `yaml:"required"`
|
||||
Properties map[string]Property `yaml:"properties"`
|
||||
}
|
||||
|
||||
type MediaType struct {
|
||||
Schema Schema `yaml:"schema"`
|
||||
}
|
||||
|
||||
// 给OpenAPI的post方法用的
|
||||
type RequestBody struct {
|
||||
Required bool `yaml:"required"`
|
||||
Content map[string]MediaType `yaml:"content"`
|
||||
}
|
||||
|
||||
type PathItem struct {
|
||||
Description string `yaml:"description"`
|
||||
Summary string `yaml:"summary"`
|
||||
OperationID string `yaml:"operationId"`
|
||||
RequestBody RequestBody `yaml:"requestBody"`
|
||||
Parameters []Parameter `yaml:"parameters"`
|
||||
Deprecated bool `yaml:"deprecated"`
|
||||
}
|
||||
|
||||
type Paths map[string]map[string]PathItem
|
||||
|
||||
type Components struct {
|
||||
Schemas map[string]interface{} `yaml:"schemas"`
|
||||
}
|
||||
|
||||
type API struct {
|
||||
OpenAPI string `yaml:"openapi"`
|
||||
Info Info `yaml:"info"`
|
||||
Servers []Server `yaml:"servers"`
|
||||
Paths Paths `yaml:"paths"`
|
||||
Components Components `yaml:"components"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
In string `yaml:"in" json:"in"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Value string `yaml:"value" json:"value"`
|
||||
}
|
||||
|
||||
type APIProvider struct {
|
||||
// @Title zh-CN 服务名称
|
||||
// @Description zh-CN 带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local
|
||||
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
|
||||
// @Title zh-CN 服务端口
|
||||
// @Description zh-CN 服务端口
|
||||
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
|
||||
// @Title zh-CN 服务域名
|
||||
// @Description zh-CN 服务域名,例如 restapi.amap.com
|
||||
Domin string `required:"true" yaml:"domain" json:"domain"`
|
||||
// @Title zh-CN 通义千问大模型服务的key
|
||||
// @Description zh-CN 通义千问大模型服务的key
|
||||
APIKey APIKey `required:"true" yaml:"apiKey" json:"apiKey"`
|
||||
}
|
||||
|
||||
type APIs struct {
|
||||
APIProvider APIProvider `required:"true" yaml:"apiProvider" json:"apiProvider"`
|
||||
API string `required:"true" yaml:"api" json:"api"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
Question string `yaml:"question" json:"question"`
|
||||
Thought1 string `yaml:"thought1" json:"thought1"`
|
||||
ActionInput string `yaml:"actionInput" json:"actionInput"`
|
||||
Observation string `yaml:"observation" json:"observation"`
|
||||
Thought2 string `yaml:"thought2" json:"thought2"`
|
||||
FinalAnswer string `yaml:"finalAnswer" json:"finalAnswer"`
|
||||
Begin string `yaml:"begin" json:"begin"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
Language string `required:"true" yaml:"language" json:"language"`
|
||||
CHTemplate Template `yaml:"chTemplate" json:"chTemplate"`
|
||||
ENTemplate Template `yaml:"enTemplate" json:"enTemplate"`
|
||||
}
|
||||
|
||||
type LLMInfo struct {
|
||||
// @Title zh-CN 大模型服务名称
|
||||
// @Description zh-CN 带服务类型的完整 FQDN 名称
|
||||
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
|
||||
// @Title zh-CN 大模型服务端口
|
||||
// @Description zh-CN 服务端口
|
||||
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
|
||||
// @Title zh-CN 大模型服务域名
|
||||
// @Description zh-CN 大模型服务域名,例如 dashscope.aliyuncs.com
|
||||
Domin string `required:"true" yaml:"domin" json:"domin"`
|
||||
// @Title zh-CN 大模型服务的key
|
||||
// @Description zh-CN 大模型服务的key
|
||||
APIKey string `required:"true" yaml:"apiKey" json:"apiKey"`
|
||||
// @Title zh-CN 大模型服务的请求路径
|
||||
// @Description zh-CN 大模型服务的请求路径,如"/compatible-mode/v1/chat/completions"
|
||||
Path string `required:"true" yaml:"path" json:"path"`
|
||||
// @Title zh-CN 大模型服务的模型名称
|
||||
// @Description zh-CN 大模型服务的模型名称,如"qwen-max-0403"
|
||||
Model string `required:"true" yaml:"model" json:"model"`
|
||||
// @Title zh-CN 结束执行循环前的最大步数
|
||||
// @Description zh-CN 结束执行循环前的最大步数,比如2,设置为0,可能会无限循环,直到超时退出,默认15
|
||||
MaxIterations int64 `yaml:"maxIterations" json:"maxIterations"`
|
||||
// @Title zh-CN 每一次请求大模型的超时时间
|
||||
// @Description zh-CN 每一次请求大模型的超时时间,单位毫秒,默认50000
|
||||
MaxExecutionTime int64 `yaml:"maxExecutionTime" json:"maxExecutionTime"`
|
||||
// @Title zh-CN
|
||||
// @Description zh-CN 每一次请求大模型的输出token限制,默认1000
|
||||
MaxTokens int64 `yaml:"maxToken" json:"maxTokens"`
|
||||
}
|
||||
|
||||
type PluginConfig struct {
|
||||
// @Title zh-CN 返回 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
ReturnResponseTemplate string `required:"true" yaml:"returnResponseTemplate" json:"returnResponseTemplate"`
|
||||
// @Title zh-CN 工具服务商以及工具信息
|
||||
// @Description zh-CN 用于存储工具服务商以及工具信息
|
||||
APIs []APIs `required:"true" yaml:"apis" json:"apis"`
|
||||
APIClient []wrapper.HttpClient `yaml:"-" json:"-"`
|
||||
// @Title zh-CN llm信息
|
||||
// @Description zh-CN 用于存储llm使用信息
|
||||
LLMInfo LLMInfo `required:"true" yaml:"llm" json:"llm"`
|
||||
LLMClient wrapper.HttpClient `yaml:"-" json:"-"`
|
||||
APIParam []APIParam `yaml:"-" json:"-"`
|
||||
PromptTemplate PromptTemplate `yaml:"promptTemplate" json:"promptTemplate"`
|
||||
}
|
||||
|
||||
func initResponsePromptTpl(gjson gjson.Result, c *PluginConfig) {
|
||||
//设置回复模板
|
||||
c.ReturnResponseTemplate = gjson.Get("returnResponseTemplate").String()
|
||||
if c.ReturnResponseTemplate == "" {
|
||||
c.ReturnResponseTemplate = `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
}
|
||||
}
|
||||
|
||||
func initAPIs(gjson gjson.Result, c *PluginConfig) error {
|
||||
//从插件配置中获取apis信息
|
||||
apis := gjson.Get("apis")
|
||||
if !apis.Exists() {
|
||||
return errors.New("apis is required")
|
||||
}
|
||||
if len(apis.Array()) == 0 {
|
||||
return errors.New("apis cannot be empty")
|
||||
}
|
||||
|
||||
for _, item := range apis.Array() {
|
||||
serviceName := item.Get("apiProvider.serviceName")
|
||||
if !serviceName.Exists() || serviceName.String() == "" {
|
||||
return errors.New("apiProvider serviceName is required")
|
||||
}
|
||||
|
||||
servicePort := item.Get("apiProvider.servicePort")
|
||||
if !servicePort.Exists() || servicePort.Int() == 0 {
|
||||
return errors.New("apiProvider servicePort is required")
|
||||
}
|
||||
|
||||
domain := item.Get("apiProvider.domain")
|
||||
if !domain.Exists() || domain.String() == "" {
|
||||
return errors.New("apiProvider domain is required")
|
||||
}
|
||||
|
||||
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
|
||||
if apiKeyIn != "query" {
|
||||
apiKeyIn = "header"
|
||||
}
|
||||
|
||||
apiKeyName := item.Get("apiProvider.apiKey.name")
|
||||
|
||||
apiKeyValue := item.Get("apiProvider.apiKey.value")
|
||||
|
||||
//根据多个toolsClientInfo的信息,分别初始化toolsClient
|
||||
apiClient := wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName.String(),
|
||||
Port: servicePort.Int(),
|
||||
Host: domain.String(),
|
||||
})
|
||||
|
||||
c.APIClient = append(c.APIClient, apiClient)
|
||||
|
||||
api := item.Get("api")
|
||||
if !api.Exists() || api.String() == "" {
|
||||
return errors.New("api is required")
|
||||
}
|
||||
|
||||
var apiStruct API
|
||||
err := yaml.Unmarshal([]byte(api.String()), &apiStruct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var allTool_param []Tool_Param
|
||||
//拆除服务下面的每个api的path
|
||||
for path, pathmap := range apiStruct.Paths {
|
||||
//拆解出每个api对应的参数
|
||||
for method, submap := range pathmap {
|
||||
//把参数列表存起来
|
||||
var param Tool_Param
|
||||
param.Path = path
|
||||
param.ToolName = submap.OperationID
|
||||
if method == "get" {
|
||||
param.Method = "GET"
|
||||
paramName := make([]string, 0)
|
||||
for _, parammeter := range submap.Parameters {
|
||||
paramName = append(paramName, parammeter.Name)
|
||||
}
|
||||
param.ParamName = paramName
|
||||
out, _ := json.Marshal(submap.Parameters)
|
||||
param.Parameter = string(out)
|
||||
param.Description = submap.Description
|
||||
} else if method == "post" {
|
||||
param.Method = "POST"
|
||||
schema := submap.RequestBody.Content["application/json"].Schema
|
||||
param.ParamName = schema.Required
|
||||
param.Description = submap.Summary
|
||||
out, _ := json.Marshal(schema.Properties)
|
||||
param.Parameter = string(out)
|
||||
}
|
||||
allTool_param = append(allTool_param, param)
|
||||
}
|
||||
}
|
||||
apiParam := APIParam{
|
||||
APIKey: APIKey{In: apiKeyIn, Name: apiKeyName.String(), Value: apiKeyValue.String()},
|
||||
URL: apiStruct.Servers[0].URL,
|
||||
Tool_Param: allTool_param,
|
||||
}
|
||||
|
||||
c.APIParam = append(c.APIParam, apiParam)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initReActPromptTpl(gjson gjson.Result, c *PluginConfig) {
|
||||
c.PromptTemplate.Language = gjson.Get("promptTemplate.language").String()
|
||||
if c.PromptTemplate.Language != "EN" && c.PromptTemplate.Language != "CH" {
|
||||
c.PromptTemplate.Language = "EN"
|
||||
}
|
||||
if c.PromptTemplate.Language == "EN" {
|
||||
c.PromptTemplate.ENTemplate.Question = gjson.Get("promptTemplate.enTemplate.question").String()
|
||||
if c.PromptTemplate.ENTemplate.Question == "" {
|
||||
c.PromptTemplate.ENTemplate.Question = "the input question you must answer"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought1").String()
|
||||
if c.PromptTemplate.ENTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.ENTemplate.Thought1 = "you should always think about what to do"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.ActionInput = gjson.Get("promptTemplate.enTemplate.actionInput").String()
|
||||
if c.PromptTemplate.ENTemplate.ActionInput == "" {
|
||||
c.PromptTemplate.ENTemplate.ActionInput = "the input to the action"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Observation = gjson.Get("promptTemplate.enTemplate.observation").String()
|
||||
if c.PromptTemplate.ENTemplate.Observation == "" {
|
||||
c.PromptTemplate.ENTemplate.Observation = "the result of the action"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought2").String()
|
||||
if c.PromptTemplate.ENTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.ENTemplate.Thought1 = "I now know the final answer"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.FinalAnswer = gjson.Get("promptTemplate.enTemplate.finalAnswer").String()
|
||||
if c.PromptTemplate.ENTemplate.FinalAnswer == "" {
|
||||
c.PromptTemplate.ENTemplate.FinalAnswer = "the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content."
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Begin = gjson.Get("promptTemplate.enTemplate.begin").String()
|
||||
if c.PromptTemplate.ENTemplate.Begin == "" {
|
||||
c.PromptTemplate.ENTemplate.Begin = "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Arg\"s"
|
||||
}
|
||||
} else if c.PromptTemplate.Language == "CH" {
|
||||
c.PromptTemplate.CHTemplate.Question = gjson.Get("promptTemplate.chTemplate.question").String()
|
||||
if c.PromptTemplate.CHTemplate.Question == "" {
|
||||
c.PromptTemplate.CHTemplate.Question = "你需要回答的输入问题"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought1").String()
|
||||
if c.PromptTemplate.CHTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.CHTemplate.Thought1 = "你应该总是思考该做什么"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.ActionInput = gjson.Get("promptTemplate.chTemplate.actionInput").String()
|
||||
if c.PromptTemplate.CHTemplate.ActionInput == "" {
|
||||
c.PromptTemplate.CHTemplate.ActionInput = "行动的输入,必须出现在Action后"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Observation = gjson.Get("promptTemplate.chTemplate.observation").String()
|
||||
if c.PromptTemplate.CHTemplate.Observation == "" {
|
||||
c.PromptTemplate.CHTemplate.Observation = "行动的结果"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought2").String()
|
||||
if c.PromptTemplate.CHTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.CHTemplate.Thought1 = "我现在知道最终答案"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.FinalAnswer = gjson.Get("promptTemplate.chTemplate.finalAnswer").String()
|
||||
if c.PromptTemplate.CHTemplate.FinalAnswer == "" {
|
||||
c.PromptTemplate.CHTemplate.FinalAnswer = "对原始输入问题的最终答案"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Begin = gjson.Get("promptTemplate.chTemplate.begin").String()
|
||||
if c.PromptTemplate.CHTemplate.Begin == "" {
|
||||
c.PromptTemplate.CHTemplate.Begin = "再次重申,不要修改以上模板的字段名称,开始吧!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initLLMClient(gjson gjson.Result, c *PluginConfig) {
|
||||
c.LLMInfo.APIKey = gjson.Get("llm.apiKey").String()
|
||||
c.LLMInfo.ServiceName = gjson.Get("llm.serviceName").String()
|
||||
c.LLMInfo.ServicePort = gjson.Get("llm.servicePort").Int()
|
||||
c.LLMInfo.Domin = gjson.Get("llm.domain").String()
|
||||
c.LLMInfo.Path = gjson.Get("llm.path").String()
|
||||
c.LLMInfo.Model = gjson.Get("llm.model").String()
|
||||
c.LLMInfo.MaxIterations = gjson.Get("llm.maxIterations").Int()
|
||||
if c.LLMInfo.MaxIterations == 0 {
|
||||
c.LLMInfo.MaxIterations = 15
|
||||
}
|
||||
c.LLMInfo.MaxExecutionTime = gjson.Get("llm.maxExecutionTime").Int()
|
||||
if c.LLMInfo.MaxExecutionTime == 0 {
|
||||
c.LLMInfo.MaxExecutionTime = 50000
|
||||
}
|
||||
c.LLMInfo.MaxTokens = gjson.Get("llm.maxTokens").Int()
|
||||
if c.LLMInfo.MaxTokens == 0 {
|
||||
c.LLMInfo.MaxTokens = 1000
|
||||
}
|
||||
|
||||
c.LLMClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.LLMInfo.ServiceName,
|
||||
Port: c.LLMInfo.ServicePort,
|
||||
Host: c.LLMInfo.Domin,
|
||||
})
|
||||
}
|
||||
46
plugins/wasm-go/extensions/ai-agent/dashscope/message.go
Normal file
46
plugins/wasm-go/extensions/ai-agent/dashscope/message.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package dashscope
|
||||
|
||||
var MessageStore ChatMessages
|
||||
|
||||
func init() {
|
||||
MessageStore = make(ChatMessages, 0)
|
||||
MessageStore.Clear() //清理和初始化
|
||||
|
||||
}
|
||||
|
||||
type ChatMessages []Message
|
||||
|
||||
// 枚举出角色
|
||||
const (
|
||||
RoleUser = "user"
|
||||
RoleAssistant = "assistant"
|
||||
RoleSystem = "system"
|
||||
)
|
||||
|
||||
func (cm *ChatMessages) Clear() {
|
||||
*cm = make([]Message, 0) //重新初始化
|
||||
}
|
||||
|
||||
// 添加角色和对应的prompt
|
||||
func (cm *ChatMessages) AddFor(msg string, role string) {
|
||||
*cm = append(*cm, Message{
|
||||
Role: role,
|
||||
Content: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// 添加Assistant角色的prompt
|
||||
func (cm *ChatMessages) AddForAssistant(msg string) {
|
||||
cm.AddFor(msg, RoleAssistant)
|
||||
|
||||
}
|
||||
|
||||
// 添加System角色的prompt
|
||||
func (cm *ChatMessages) AddForSystem(msg string) {
|
||||
cm.AddFor(msg, RoleSystem)
|
||||
}
|
||||
|
||||
// 添加User角色的prompt
|
||||
func (cm *ChatMessages) AddForUser(msg string) {
|
||||
cm.AddFor(msg, RoleUser)
|
||||
}
|
||||
70
plugins/wasm-go/extensions/ai-agent/dashscope/types.go
Normal file
70
plugins/wasm-go/extensions/ai-agent/dashscope/types.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package dashscope
|
||||
|
||||
// DashScope embedding service: Request
|
||||
type Request struct {
|
||||
Model string `json:"model"`
|
||||
Input Input `json:"input"`
|
||||
Parameter Parameter `json:"parameters"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Texts []string `json:"texts"`
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
TextType string `json:"text_type"`
|
||||
}
|
||||
|
||||
// DashScope embedding service: Response
|
||||
type Response struct {
|
||||
Output Output `json:"output"`
|
||||
Usage Usage `json:"usage"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Embeddings []Embedding `json:"embeddings"`
|
||||
}
|
||||
|
||||
type Embedding struct {
|
||||
Embedding []float32 `json:"embedding"`
|
||||
TextIndex int32 `json:"text_index"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
TotalTokens int32 `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// completion
|
||||
type Completion struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
MaxTokens int64 `json:"max_tokens"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type CompletionResponse struct {
|
||||
Choices []Choice `json:"choices"`
|
||||
Object string `json:"object"`
|
||||
Usage CompletionUsage `json:"usage"`
|
||||
Created string `json:"created"`
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
Model string `json:"model"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type CompletionUsage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
19
plugins/wasm-go/extensions/ai-agent/go.mod
Normal file
19
plugins/wasm-go/extensions/ai-agent/go.mod
Normal file
@@ -0,0 +1,19 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
)
|
||||
26
plugins/wasm-go/extensions/ai-agent/go.sum
Normal file
26
plugins/wasm-go/extensions/ai-agent/go.sum
Normal file
@@ -0,0 +1,26 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2 h1:gH7OIGXm4wtW5Vo7L2deMPqF7OVWNESDHv1CaaTGu6s=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2/go.mod h1:359don/ahMxpfeLMzr29Cjwcu8IywTTDUzWlBPRNLHw=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
372
plugins/wasm-go/extensions/ai-agent/main.go
Normal file
372
plugins/wasm-go/extensions/ai-agent/main.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/dashscope"
|
||||
prompttpl "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/promptTpl"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// 用于统计函数的递归调用次数
|
||||
const ToolCallsCount = "ToolCallsCount"
|
||||
|
||||
// react的正则规则
|
||||
const ActionPattern = `Action:\s*(.*?)[.\n]`
|
||||
const ActionInputPattern = `Action Input:\s*(.*)`
|
||||
const FinalAnswerPattern = `Final Answer:(.*)`
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"ai-agent",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(gjson gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
initResponsePromptTpl(gjson, c)
|
||||
|
||||
err := initAPIs(gjson, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
initReActPromptTpl(gjson, c)
|
||||
|
||||
initLLMClient(gjson, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
|
||||
log.Debugf("[onHttpRequestBody] firstreq:%s", prompt)
|
||||
|
||||
var userMessage Message
|
||||
userMessage.Role = "user"
|
||||
userMessage.Content = prompt
|
||||
|
||||
newMessages := []Message{userMessage}
|
||||
rawRequest.Messages = newMessages
|
||||
|
||||
//replace old message and resume request qwen
|
||||
newbody, err := json.Marshal(rawRequest)
|
||||
if err != nil {
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
log.Debugf("[onHttpRequestBody] newRequestBody: ", string(newbody))
|
||||
err := proxywasm.ReplaceHttpRequestBody(newbody)
|
||||
if err != nil {
|
||||
log.Debug("替换失败")
|
||||
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, "替换失败"+err.Error())), -1)
|
||||
}
|
||||
log.Debug("[onHttpRequestBody] request替换成功")
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
log.Debug("onHttpRequestBody start")
|
||||
defer log.Debug("onHttpRequestBody end")
|
||||
|
||||
//拿到请求
|
||||
var rawRequest Request
|
||||
err := json.Unmarshal(body, &rawRequest)
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] body json umarshal err: ", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Debugf("onHttpRequestBody rawRequest: %v", rawRequest)
|
||||
|
||||
//获取用户query
|
||||
var query string
|
||||
messageLength := len(rawRequest.Messages)
|
||||
log.Debugf("[onHttpRequestBody] messageLength: %s\n", messageLength)
|
||||
if messageLength > 0 {
|
||||
query = rawRequest.Messages[messageLength-1].Content
|
||||
log.Debugf("[onHttpRequestBody] query: %s\n", query)
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
log.Debug("parse query from request body failed")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
//拼装agent prompt模板
|
||||
tool_desc := make([]string, 0)
|
||||
tool_names := make([]string, 0)
|
||||
for _, apiParam := range config.APIParam {
|
||||
for _, tool_param := range apiParam.Tool_Param {
|
||||
tool_desc = append(tool_desc, fmt.Sprintf(prompttpl.TOOL_DESC, tool_param.ToolName, tool_param.Description, tool_param.Description, tool_param.Description, tool_param.Parameter), "\n")
|
||||
tool_names = append(tool_names, tool_param.ToolName)
|
||||
}
|
||||
}
|
||||
|
||||
var prompt string
|
||||
if config.PromptTemplate.Language == "CH" {
|
||||
prompt = fmt.Sprintf(prompttpl.CH_Template,
|
||||
tool_desc,
|
||||
config.PromptTemplate.CHTemplate.Question,
|
||||
config.PromptTemplate.CHTemplate.Thought1,
|
||||
tool_names,
|
||||
config.PromptTemplate.CHTemplate.ActionInput,
|
||||
config.PromptTemplate.CHTemplate.Observation,
|
||||
config.PromptTemplate.CHTemplate.Thought2,
|
||||
config.PromptTemplate.CHTemplate.FinalAnswer,
|
||||
config.PromptTemplate.CHTemplate.Begin,
|
||||
query)
|
||||
} else {
|
||||
prompt = fmt.Sprintf(prompttpl.EN_Template,
|
||||
tool_desc,
|
||||
config.PromptTemplate.ENTemplate.Question,
|
||||
config.PromptTemplate.ENTemplate.Thought1,
|
||||
tool_names,
|
||||
config.PromptTemplate.ENTemplate.ActionInput,
|
||||
config.PromptTemplate.ENTemplate.Observation,
|
||||
config.PromptTemplate.ENTemplate.Thought2,
|
||||
config.PromptTemplate.ENTemplate.FinalAnswer,
|
||||
config.PromptTemplate.ENTemplate.Begin,
|
||||
query)
|
||||
}
|
||||
|
||||
ctx.SetContext(ToolCallsCount, 0)
|
||||
|
||||
//清理历史对话记录
|
||||
dashscope.MessageStore.Clear()
|
||||
|
||||
//将请求加入到历史对话存储器中
|
||||
dashscope.MessageStore.AddForUser(prompt)
|
||||
|
||||
//开始第一次请求
|
||||
ret := firstReq(config, prompt, rawRequest, log)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
log.Debug("onHttpResponseHeaders start")
|
||||
defer log.Debug("onHttpResponseHeaders end")
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Debugf("statusCode: %d\n", statusCode)
|
||||
}
|
||||
log.Info("========函数返回结果========")
|
||||
log.Infof(string(responseBody))
|
||||
|
||||
observation := "Observation: " + string(responseBody)
|
||||
|
||||
dashscope.MessageStore.AddForUser(observation)
|
||||
|
||||
completion := dashscope.Completion{
|
||||
Model: config.LLMInfo.Model,
|
||||
Messages: dashscope.MessageStore,
|
||||
MaxTokens: config.LLMInfo.MaxTokens,
|
||||
}
|
||||
|
||||
headers := [][2]string{{"Content-Type", "application/json"}, {"Authorization", "Bearer " + config.LLMInfo.APIKey}}
|
||||
completionSerialized, _ := json.Marshal(completion)
|
||||
err := config.LLMClient.Post(
|
||||
config.LLMInfo.Path,
|
||||
headers,
|
||||
completionSerialized,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
//得到gpt的返回结果
|
||||
var responseCompletion dashscope.CompletionResponse
|
||||
_ = json.Unmarshal(responseBody, &responseCompletion)
|
||||
log.Infof("[toolsCall] content: %s\n", responseCompletion.Choices[0].Message.Content)
|
||||
|
||||
if responseCompletion.Choices[0].Message.Content != "" {
|
||||
retType := toolsCall(ctx, config, responseCompletion.Choices[0].Message.Content, rawResponse, log)
|
||||
if retType == types.ActionContinue {
|
||||
//得到了Final Answer
|
||||
var assistantMessage Message
|
||||
assistantMessage.Role = "assistant"
|
||||
startIndex := strings.Index(responseCompletion.Choices[0].Message.Content, "Final Answer:")
|
||||
if startIndex != -1 {
|
||||
startIndex += len("Final Answer:") // 移动到"Final Answer:"之后的位置
|
||||
extractedText := responseCompletion.Choices[0].Message.Content[startIndex:]
|
||||
assistantMessage.Content = extractedText
|
||||
}
|
||||
|
||||
rawResponse.Choices[0].Message = assistantMessage
|
||||
|
||||
newbody, err := json.Marshal(rawResponse)
|
||||
if err != nil {
|
||||
proxywasm.ResumeHttpResponse()
|
||||
return
|
||||
} else {
|
||||
log.Infof("[onHttpResponseBody] newResponseBody: ", string(newbody))
|
||||
proxywasm.ReplaceHttpResponseBody(newbody)
|
||||
|
||||
log.Debug("[onHttpResponseBody] response替换成功")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}, uint32(config.LLMInfo.MaxExecutionTime))
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] completion err: %s", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) types.Action {
|
||||
dashscope.MessageStore.AddForAssistant(content)
|
||||
|
||||
//得到最终答案
|
||||
regexPattern := regexp.MustCompile(FinalAnswerPattern)
|
||||
finalAnswer := regexPattern.FindStringSubmatch(content)
|
||||
if len(finalAnswer) > 1 {
|
||||
return types.ActionContinue
|
||||
}
|
||||
count := ctx.GetContext(ToolCallsCount).(int)
|
||||
count++
|
||||
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d\n", count, config.LLMInfo.MaxIterations)
|
||||
//函数递归调用次数,达到了预设的循环次数,强制结束
|
||||
if int64(count) > config.LLMInfo.MaxIterations {
|
||||
ctx.SetContext(ToolCallsCount, 0)
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
ctx.SetContext(ToolCallsCount, count)
|
||||
}
|
||||
|
||||
//没得到最终答案
|
||||
regexAction := regexp.MustCompile(ActionPattern)
|
||||
regexActionInput := regexp.MustCompile(ActionInputPattern)
|
||||
|
||||
action := regexAction.FindStringSubmatch(content)
|
||||
actionInput := regexActionInput.FindStringSubmatch(content)
|
||||
|
||||
if len(action) > 1 && len(actionInput) > 1 {
|
||||
var url string
|
||||
var headers [][2]string
|
||||
var apiClient wrapper.HttpClient
|
||||
var method string
|
||||
var reqBody []byte
|
||||
var key string
|
||||
|
||||
for i, apiParam := range config.APIParam {
|
||||
for _, tool_param := range apiParam.Tool_Param {
|
||||
if action[1] == tool_param.ToolName {
|
||||
log.Infof("calls %s\n", tool_param.ToolName)
|
||||
log.Infof("actionInput[1]: %s", actionInput[1])
|
||||
|
||||
//将大模型需要的参数反序列化
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(actionInput[1]), &data); err != nil {
|
||||
log.Debugf("Error: %s\n", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
method = tool_param.Method
|
||||
|
||||
//key or header组装
|
||||
if apiParam.APIKey.Name != "" {
|
||||
if apiParam.APIKey.In == "query" { //query类型的key要放到url中
|
||||
headers = nil
|
||||
key = "?" + apiParam.APIKey.Name + "=" + apiParam.APIKey.Value
|
||||
} else if apiParam.APIKey.In == "header" { //header类型的key放在header中
|
||||
headers = [][2]string{{"Content-Type", "application/json"}, {"Authorization", apiParam.APIKey.Name + " " + apiParam.APIKey.Value}}
|
||||
}
|
||||
}
|
||||
|
||||
if method == "GET" {
|
||||
//query组装
|
||||
var args string
|
||||
for i, param := range tool_param.ParamName { //从参数列表中取出参数
|
||||
if i == 0 && apiParam.APIKey.In != "query" {
|
||||
args = "?" + param + "=%s"
|
||||
args = fmt.Sprintf(args, data[param])
|
||||
} else {
|
||||
args = args + "&" + param + "=%s"
|
||||
args = fmt.Sprintf(args, data[param])
|
||||
}
|
||||
}
|
||||
|
||||
//url组装
|
||||
url = apiParam.URL + tool_param.Path + key + args
|
||||
} else if method == "POST" {
|
||||
reqBody = nil
|
||||
//json参数组装
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Debugf("Error: %s\n", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
reqBody = jsonData
|
||||
|
||||
//url组装
|
||||
url = apiParam.URL + tool_param.Path + key
|
||||
}
|
||||
|
||||
log.Infof("url: %s\n", url)
|
||||
|
||||
apiClient = config.APIClient[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if apiClient != nil {
|
||||
err := apiClient.Call(
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
reqBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
toolsCallResult(ctx, config, content, rawResponse, log, statusCode, responseBody)
|
||||
}, 50000)
|
||||
if err != nil {
|
||||
log.Debugf("tool calls error: %s\n", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// 从response接收到firstreq的大模型返回
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
log.Debugf("onHttpResponseBody start")
|
||||
defer log.Debugf("onHttpResponseBody end")
|
||||
|
||||
//初始化接收gpt返回内容的结构体
|
||||
var rawResponse Response
|
||||
err := json.Unmarshal(body, &rawResponse)
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpResponseBody] body to json err: %s", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Infof("first content: %s\n", rawResponse.Choices[0].Message.Content)
|
||||
//如果gpt返回的内容不是空的
|
||||
if rawResponse.Choices[0].Message.Content != "" {
|
||||
//进入agent的循环思考,工具调用的过程中
|
||||
return toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
93
plugins/wasm-go/extensions/ai-agent/promptTpl/prompt.go
Normal file
93
plugins/wasm-go/extensions/ai-agent/promptTpl/prompt.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package prompttpl
|
||||
|
||||
// input param
|
||||
// {name_for_model}
|
||||
// {description_for_model}
|
||||
// {description_for_model}
|
||||
// {description_for_model}
|
||||
// {parameters}
|
||||
const TOOL_DESC = `
|
||||
%s: Call this tool to interact with the %s API. What is the %s API useful for? %s
|
||||
Parameters:
|
||||
%s
|
||||
Format the arguments as a JSON object.`
|
||||
|
||||
/*
|
||||
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
|
||||
|
||||
%s
|
||||
|
||||
Use the following format:
|
||||
|
||||
Question: the input question you must answer
|
||||
Thought: you should always think about what to do
|
||||
Action: the action to take, should be one of %s
|
||||
Action Input: the input to the action
|
||||
Observation: the result of the action
|
||||
... (this Thought/Action/Action Input/Observation can repeat N times)
|
||||
Thought: I now know the final answer
|
||||
Final Answer: the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content.
|
||||
|
||||
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s
|
||||
|
||||
Question: %s
|
||||
*/
|
||||
const EN_Template = `
|
||||
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
|
||||
|
||||
%s
|
||||
|
||||
Use the following format:
|
||||
|
||||
Question: %s
|
||||
Thought: %s
|
||||
Action: the action to take, should be one of %s
|
||||
Action Input: %s
|
||||
Observation: %s
|
||||
... (this Thought/Action/Action Input/Observation can repeat N times)
|
||||
Thought: %s
|
||||
Final Answer: %s
|
||||
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
`
|
||||
|
||||
/*
|
||||
尽你所能回答以下问题。你可以使用以下工具:
|
||||
|
||||
%s
|
||||
|
||||
请使用以下格式,其中Action字段后必须跟着Action Input字段,并且不要将Action Input替换成Input或者tool等字段,不能出现格式以外的字段名,每个字段在每个轮次只出现一次:
|
||||
Question: 你需要回答的输入问题
|
||||
Thought: 你应该总是思考该做什么
|
||||
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
|
||||
Action Input: 行动的输入,必须出现在Action后。
|
||||
Observation: 行动的结果
|
||||
...(这个Thought/Action/Action Input/Observation可以重复N次)
|
||||
Thought: 我现在知道最终答案
|
||||
Final Answer: 对原始输入问题的最终答案
|
||||
|
||||
再次重申,不要修改以上模板的字段名称,开始吧!
|
||||
|
||||
Question: %s
|
||||
*/
|
||||
const CH_Template = `
|
||||
尽你所能回答以下问题。你可以使用以下工具:
|
||||
|
||||
%s
|
||||
|
||||
请使用以下格式,其中Action字段后必须跟着Action Input字段,并且不要将Action Input替换成Input或者tool等字段,不能出现格式以外的字段名,每个字段在每个轮次只出现一次:
|
||||
Question: %s
|
||||
Thought: %s
|
||||
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
|
||||
Action Input: %s
|
||||
Observation: %s
|
||||
...(这个Thought/Action/Action Input/Observation可以重复N次)
|
||||
Thought: %s
|
||||
Final Answer: %s
|
||||
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
`
|
||||
1
plugins/wasm-go/extensions/ai-cache/.buildrc
Normal file
1
plugins/wasm-go/extensions/ai-cache/.buildrc
Normal file
@@ -0,0 +1 @@
|
||||
EXTRA_TAGS=proxy_wasm_version_0_2_100
|
||||
@@ -32,3 +32,15 @@ redis:
|
||||
serviceName: my-redis.dns
|
||||
timeout: 2000
|
||||
```
|
||||
|
||||
## 进阶用法
|
||||
|
||||
当前默认的缓存 key 是基于 GJSON PATH 的表达式:`messages.@reverse.0.content` 提取,含义是把 messages 数组反转后取第一项的 content;
|
||||
|
||||
GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key,可以写成: `messages.@reverse.#(role=="user").content`;
|
||||
|
||||
如果希望将所有 role 为 user 的 content 拼成一个数组作为 key,可以写成:`messages.@reverse.#(role=="user")#.content`;
|
||||
|
||||
还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key,可以写成:`messages.@reverse.#(role=="user")#.content|1`。
|
||||
|
||||
更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。
|
||||
|
||||
@@ -8,7 +8,7 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240528060522-53bccf89f441
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
github.com/tidwall/resp v0.1.1
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
|
||||
@@ -5,6 +5,7 @@ github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbG
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -222,9 +222,9 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
log.Debugf("cache hit, key:%s", key)
|
||||
ctx.SetContext(CacheKeyContextKey, nil)
|
||||
if !stream {
|
||||
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, response.String())), -1)
|
||||
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, response.String())), -1)
|
||||
} else {
|
||||
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnStreamResponseTemplate, response.String())), -1)
|
||||
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnStreamResponseTemplate, response.String())), -1)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
# 简介
|
||||
AI提示词修饰插件,通过在与大模型发起的请求前后插入指定信息来调整大模型的输出。
|
||||
AI提示词装饰器插件,支持在LLM的请求前后插入prompt。
|
||||
|
||||
# 配置说明
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|-----------------|------|-----|----------------------------------|
|
||||
| `decorators` | array of object | 必填 | - | 修饰设置 |
|
||||
|
||||
template object 配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|-----------------|------|-----|----------------------------------|
|
||||
| `name` | string | 必填 | - | 修饰名称 |
|
||||
| `decorator.prepend` | array of message object | 必填 | - | 在初始输入之前插入的语句 |
|
||||
| `decorator.append` | array of message object | 必填 | - | 在初始输入之后插入的语句 |
|
||||
| `prepend` | array of message object | optional | - | 在初始输入之前插入的语句 |
|
||||
| `append` | array of message object | optional | - | 在初始输入之后插入的语句 |
|
||||
|
||||
message object 配置说明:
|
||||
|
||||
@@ -26,57 +20,116 @@ message object 配置说明:
|
||||
配置示例如下:
|
||||
|
||||
```yaml
|
||||
decorators:
|
||||
- name: "hangzhou-guide"
|
||||
decorator:
|
||||
prepend:
|
||||
- role: system
|
||||
content: "You will always respond in the Chinese language."
|
||||
- role: user
|
||||
content: "Assume you are from Hangzhou."
|
||||
append:
|
||||
- role: user
|
||||
content: "Don't introduce Hangzhou's food."
|
||||
prepend:
|
||||
- role: system
|
||||
content: "请使用英语回答问题"
|
||||
append:
|
||||
- role: user
|
||||
content: "每次回答完问题,尝试进行反问"
|
||||
```
|
||||
|
||||
使用以上配置发起请求:
|
||||
|
||||
```bash
|
||||
{
|
||||
curl http://localhost/test \
|
||||
-H "content-type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please introduce your home."
|
||||
"content": "你是谁?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
响应如下:
|
||||
经过插件处理后,实际请求为:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "chatcmpl-9UYwQlEg6GwAswEZBDYXl41RU4gab",
|
||||
"object": "chat.completion",
|
||||
"created": 1717071182,
|
||||
"model": "gpt-3.5-turbo-0125",
|
||||
"choices": [
|
||||
```bash
|
||||
curl http://localhost/test \
|
||||
-H "content-type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "杭州是一个美丽的城市,有着悠久的历史和富有特色的文化。这里风景优美,有西湖、雷峰塔等著名景点,吸引着许多游客前来观光。杭州人民热情好客,城市宁静安逸,是一个适合居住和旅游的地方。"
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
"role": "system",
|
||||
"content": "请使用英语回答问题"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你是谁?"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "每次回答完问题,尝试进行反问"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 49,
|
||||
"completion_tokens": 117,
|
||||
"total_tokens": 166
|
||||
},
|
||||
"system_fingerprint": null
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
# 基于geo-ip插件的能力,扩展AI提示词装饰器插件携带用户地理位置信息
|
||||
如果需要在LLM的请求前后加入用户地理位置信息,请确保同时开启geo-ip插件和AI提示词装饰器插件。并且在相同的请求处理阶段里,geo-ip插件的优先级必须高于AI提示词装饰器插件。首先geo-ip插件会根据用户ip计算出用户的地理位置信息,然后通过请求属性传递给后续插件。比如在默认阶段里,geo-ip插件的priority配置1000,ai-prompt-decorator插件的priority配置500。
|
||||
|
||||
geo-ip插件配置示例:
|
||||
```yaml
|
||||
ipProtocal: "ipv4"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
AI提示词装饰器插件的配置示例如下:
|
||||
```yaml
|
||||
prepend:
|
||||
- role: system
|
||||
content: "提问用户当前的地理位置信息是,国家:${geo-country},省份:${geo-province}, 城市:${geo-city}"
|
||||
append:
|
||||
- role: user
|
||||
content: "每次回答完问题,尝试进行反问"
|
||||
```
|
||||
|
||||
使用以上配置发起请求:
|
||||
|
||||
```bash
|
||||
curl http://localhost/test \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-forwarded-for: 87.254.207.100,4.5.6.7" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "今天天气怎么样?"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
经过插件处理后,实际请求为:
|
||||
|
||||
```bash
|
||||
curl http://localhost/test \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-forwarded-for: 87.254.207.100,4.5.6.7" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "提问用户当前的地理位置信息是,国家:中国,省份:北京, 城市:北京"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "今天天气怎么样?"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "每次回答完问题,尝试进行反问"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ module ai-prompt-decorator
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5 h1:VOLL3m442IHCSu8mR5AZ4sc6LVT9X0w1hdqDI7oB9jY=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5/go.mod h1:kr3V9Ntbspj1eSrX8rgjBsdMXkGupYEf+LM72caGPQc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
@@ -20,66 +21,91 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type AIPromptDecoratorConfig struct {
|
||||
decorators map[string]string
|
||||
Prepend []Message `json:"prepend"`
|
||||
Append []Message `json:"append"`
|
||||
}
|
||||
|
||||
func removeBrackets(raw string) (string, error) {
|
||||
startIndex := strings.Index(raw, "{")
|
||||
endIndex := strings.LastIndex(raw, "}")
|
||||
if startIndex == -1 || endIndex == -1 {
|
||||
return raw, errors.New("message format is wrong!")
|
||||
} else {
|
||||
return raw[startIndex : endIndex+1], nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *AIPromptDecoratorConfig, log wrapper.Log) error {
|
||||
config.decorators = make(map[string]string)
|
||||
for _, v := range json.Get("decorators").Array() {
|
||||
config.decorators[v.Get("name").String()] = v.Get("decorator").Raw
|
||||
// log.Info(v.Get("decorator").Raw)
|
||||
}
|
||||
return nil
|
||||
func parseConfig(jsonConfig gjson.Result, config *AIPromptDecoratorConfig, log wrapper.Log) error {
|
||||
return json.Unmarshal([]byte(jsonConfig.Raw), config)
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIPromptDecoratorConfig, log wrapper.Log) types.Action {
|
||||
decorator, _ := proxywasm.GetHttpRequestHeader("decorator")
|
||||
if decorator == "" {
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.SetContext("decorator", decorator)
|
||||
proxywasm.RemoveHttpRequestHeader("decorator")
|
||||
proxywasm.RemoveHttpRequestHeader("content-length")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config AIPromptDecoratorConfig, body []byte, log wrapper.Log) types.Action {
|
||||
decoratorName := ctx.GetContext("decorator").(string)
|
||||
decorator := config.decorators[decoratorName]
|
||||
func replaceVariable(variable string, entry *Message) (*Message, error) {
|
||||
key := fmt.Sprintf("${%s}", variable)
|
||||
if strings.Contains(entry.Content, key) {
|
||||
value, err := proxywasm.GetProperty([]string{variable})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.Content = strings.ReplaceAll(entry.Content, key, string(value))
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func decorateGeographicPrompt(entry *Message) (*Message, error) {
|
||||
geoArr := []string{"geo-country", "geo-province", "geo-city", "geo-isp"}
|
||||
|
||||
var err error
|
||||
for _, geo := range geoArr {
|
||||
entry, err = replaceVariable(geo, entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config AIPromptDecoratorConfig, body []byte, log wrapper.Log) types.Action {
|
||||
messageJson := `{"messages":[]}`
|
||||
|
||||
prependMessage := gjson.Get(decorator, "prepend")
|
||||
if prependMessage.Exists() {
|
||||
for _, entry := range prependMessage.Array() {
|
||||
messageJson, _ = sjson.SetRaw(messageJson, "messages.-1", entry.Raw)
|
||||
for _, entry := range config.Prepend {
|
||||
entry, err := decorateGeographicPrompt(&entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to decorate geographic prompt in prepend, error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add prepend message, error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
messageJson, _ = sjson.SetRaw(messageJson, "messages.-1", string(msg))
|
||||
}
|
||||
|
||||
rawMessage := gjson.GetBytes(body, "messages")
|
||||
if rawMessage.Exists() {
|
||||
for _, entry := range rawMessage.Array() {
|
||||
messageJson, _ = sjson.SetRaw(messageJson, "messages.-1", entry.Raw)
|
||||
}
|
||||
if !rawMessage.Exists() {
|
||||
log.Errorf("Cannot find messages field in request body")
|
||||
return types.ActionContinue
|
||||
}
|
||||
for _, entry := range rawMessage.Array() {
|
||||
messageJson, _ = sjson.SetRaw(messageJson, "messages.-1", entry.Raw)
|
||||
}
|
||||
|
||||
appendMessage := gjson.Get(decorator, "append")
|
||||
if appendMessage.Exists() {
|
||||
for _, entry := range appendMessage.Array() {
|
||||
messageJson, _ = sjson.SetRaw(messageJson, "messages.-1", entry.Raw)
|
||||
for _, entry := range config.Append {
|
||||
entry, err := decorateGeographicPrompt(&entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to decorate geographic prompt in append, error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add prepend message, error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
messageJson, _ = sjson.SetRaw(messageJson, "messages.-1", string(msg))
|
||||
}
|
||||
|
||||
newbody, err := sjson.SetRaw(string(body), "messages", gjson.Get(messageJson, "messages").Raw)
|
||||
|
||||
@@ -2,9 +2,11 @@ module ai-prompt-template
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbG
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
1
plugins/wasm-go/extensions/ai-proxy/.buildrc
Normal file
1
plugins/wasm-go/extensions/ai-proxy/.buildrc
Normal file
@@ -0,0 +1 @@
|
||||
EXTRA_TAGS=proxy_wasm_version_0_2_100
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: AI 代理
|
||||
keywords: [ higress,ai,proxy,rag ]
|
||||
keywords: [ AI网关, AI代理 ]
|
||||
description: AI 代理插件配置参考
|
||||
---
|
||||
|
||||
@@ -9,6 +9,13 @@ description: AI 代理插件配置参考
|
||||
`AI 代理`插件实现了基于 OpenAI API 契约的 AI 代理功能。目前支持 OpenAI、Azure OpenAI、月之暗面(Moonshot)和通义千问等 AI
|
||||
服务提供商。
|
||||
|
||||
> **注意:**
|
||||
|
||||
> 请求路径后缀匹配 `/v1/chat/completions` 时,对应文生文场景,会用 OpenAI 的文生文协议解析请求 Body,再转换为对应 LLM 厂商的文生文协议
|
||||
|
||||
> 请求路径后缀匹配 `/v1/embeddings` 时,对应文本向量场景,会用 OpenAI 的文本向量协议解析请求 Body,再转换为对应 LLM 厂商的文本向量协议
|
||||
|
||||
|
||||
## 配置字段
|
||||
|
||||
### 基本配置
|
||||
@@ -19,14 +26,15 @@ description: AI 代理插件配置参考
|
||||
|
||||
`provider`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------------- | --------------- | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `type` | string | 必填 | - | AI 服务提供商名称 |
|
||||
| `apiTokens` | array of string | 必填 | - | 用于在访问 AI 服务时进行认证的令牌。如果配置了多个 token,插件会在请求时随机进行选择。部分服务提供商只支持配置一个 token。 |
|
||||
| `timeout` | number | 非必填 | - | 访问 AI 服务的超时时间。单位为毫秒。默认值为 120000,即 2 分钟 |
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>可以使用 "*" 为键来配置通用兜底映射关系 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------------- | --------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | string | 必填 | - | AI 服务提供商名称 |
|
||||
| `apiTokens` | array of string | 非必填 | - | 用于在访问 AI 服务时进行认证的令牌。如果配置了多个 token,插件会在请求时随机进行选择。部分服务提供商只支持配置一个 token。 |
|
||||
| `timeout` | number | 非必填 | - | 访问 AI 服务的超时时间。单位为毫秒。默认值为 120000,即 2 分钟 |
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
| `customSettings` | array of customSetting | 非必填 | - | 为AI请求指定覆盖或者填充参数 |
|
||||
|
||||
`context`的配置字段说明如下:
|
||||
|
||||
@@ -36,11 +44,44 @@ description: AI 代理插件配置参考
|
||||
| `serviceName` | string | 必填 | - | URL 所对应的 Higress 后端服务完整名称 |
|
||||
| `servicePort` | number | 必填 | - | URL 所对应的 Higress 后端服务访问端口 |
|
||||
|
||||
|
||||
`customSettings`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | string | 必填 | - | 想要设置的参数的名称,例如`max_tokens` |
|
||||
| `value` | string/int/float/bool | 必填 | - | 想要设置的参数的值,例如0 |
|
||||
| `mode` | string | 非必填 | "auto" | 参数设置的模式,可以设置为"auto"或者"raw",如果为"auto"则会自动根据协议对参数名做改写,如果为"raw"则不会有任何改写和限制检查 |
|
||||
| `overwrite` | bool | 非必填 | true | 如果为false则只在用户没有设置这个参数时填充参数,否则会直接覆盖用户原有的参数设置 |
|
||||
|
||||
|
||||
custom-setting会遵循如下表格,根据`name`和协议来替换对应的字段,用户需要填写表格中`settingName`列中存在的值。例如用户将`name`设置为`max_tokens`,在openai协议中会替换`max_tokens`,在gemini中会替换`maxOutputTokens`。
|
||||
`none`表示该协议不支持此参数。如果`name`不在此表格中或者对应协议不支持此参数,同时没有设置raw模式,则配置不会生效。
|
||||
|
||||
|
||||
| settingName | openai | baidu | spark | qwen | gemini | hunyuan | claude | minimax |
|
||||
| ----------- | ----------- | ----------------- | ----------- | ----------- | --------------- | ----------- | ----------- | ------------------ |
|
||||
| max_tokens | max_tokens | max_output_tokens | max_tokens | max_tokens | maxOutputTokens | none | max_tokens | tokens_to_generate |
|
||||
| temperature | temperature | temperature | temperature | temperature | temperature | Temperature | temperature | temperature |
|
||||
| top_p | top_p | top_p | none | top_p | topP | TopP | top_p | top_p |
|
||||
| top_k | none | none | top_k | none | topK | none | top_k | none |
|
||||
| seed | seed | none | none | seed | none | none | none | none |
|
||||
|
||||
如果启用了raw模式,custom-setting会直接用输入的`name`和`value`去更改请求中的json内容,而不对参数名称做任何限制和修改。
|
||||
对于大多数协议,custom-setting都会在json内容的根路径修改或者填充参数。对于`qwen`协议,ai-proxy会在json的`parameters`子路径下做配置。对于`gemini`协议,则会在`generation_config`子路径下做配置。
|
||||
|
||||
|
||||
### 提供商特有配置
|
||||
|
||||
#### OpenAI
|
||||
|
||||
OpenAI 所对应的 `type` 为 `openai`。它并无特有的配置字段。
|
||||
OpenAI 所对应的 `type` 为 `openai`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|----------|----------|--------|-------------------------------------------------------------------------------|
|
||||
| `openaiCustomUrl` | string | 非必填 | - | 基于OpenAI协议的自定义后端URL,例如: www.example.com/myai/v1/chat/completions |
|
||||
| `responseJsonSchema` | object | 非必填 | - | 预先定义OpenAI响应需满足的Json Schema, 注意目前仅特定的几种模型支持该用法|
|
||||
|
||||
|
||||
#### Azure OpenAI
|
||||
|
||||
@@ -93,6 +134,10 @@ Groq 所对应的 `type` 为 `groq`。它并无特有的配置字段。
|
||||
|
||||
文心一言所对应的 `type` 为 `baidu`。它并无特有的配置字段。
|
||||
|
||||
#### 360智脑
|
||||
|
||||
360智脑所对应的 `type` 为 `ai360`。它并无特有的配置字段。
|
||||
|
||||
#### MiniMax
|
||||
|
||||
MiniMax所对应的 `type` 为 `minimax`。它特有的配置字段如下:
|
||||
@@ -131,6 +176,36 @@ Ollama 所对应的 `type` 为 `ollama`。它特有的配置字段如下:
|
||||
|
||||
阶跃星辰所对应的 `type` 为 `stepfun`。它并无特有的配置字段。
|
||||
|
||||
#### Cloudflare Workers AI
|
||||
|
||||
Cloudflare Workers AI 所对应的 `type` 为 `cloudflare`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|--------|------|-----|----------------------------------------------------------------------------------------------------------------------------|
|
||||
| `cloudflareAccountId` | string | 必填 | - | [Cloudflare Account ID](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-api-token-and-account-id) |
|
||||
|
||||
#### 星火 (Spark)
|
||||
|
||||
星火所对应的 `type` 为 `spark`。它并无特有的配置字段。
|
||||
|
||||
讯飞星火认知大模型的`apiTokens`字段值为`APIKey:APISecret`。即填入自己的APIKey与APISecret,并以`:`分隔。
|
||||
|
||||
#### Gemini
|
||||
|
||||
Gemini 所对应的 `type` 为 `gemini`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| --------------------- | -------- | -------- |-----|-------------------------------------------------------------------------------------------------|
|
||||
| `geminiSafetySetting` | map of string | 非必填 | - | Gemini AI内容过滤和安全级别设定。参考[Safety settings](https://ai.google.dev/gemini-api/docs/safety-settings) |
|
||||
|
||||
#### DeepL
|
||||
|
||||
DeepL 所对应的 `type` 为 `deepl`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------ | -------- | -------- | ------ | ---------------------------- |
|
||||
| `targetLang` | string | 必填 | - | DeepL 翻译服务需要的目标语种 |
|
||||
|
||||
## 用法示例
|
||||
|
||||
### 使用 OpenAI 协议代理 Azure OpenAI 服务
|
||||
@@ -246,10 +321,17 @@ provider:
|
||||
'gpt-3': "qwen-turbo"
|
||||
'gpt-35-turbo': "qwen-plus"
|
||||
'gpt-4-turbo': "qwen-max"
|
||||
'gpt-4-*': "qwen-max"
|
||||
'gpt-4o': "qwen-vl-plus"
|
||||
'text-embedding-v1': 'text-embedding-v1'
|
||||
'*': "qwen-turbo"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
**AI 对话请求示例**
|
||||
|
||||
URL: http://your-domain/v1/chat/completions
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -264,7 +346,7 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -290,6 +372,110 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
**多模态模型 API 请求示例(适用于 `qwen-vl-plus` 和 `qwen-vl-max` 模型)**
|
||||
|
||||
URL: http://your-domain/v1/chat/completions
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个图片是哪里?"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "17c5955d-af9c-9f28-bbde-293a9c9a3515",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"text": "这张照片显示的是一位女士和一只狗在海滩上。由于我无法获取具体的地理位置信息,所以不能确定这是哪个地方的海滩。但是从视觉内容来看,它可能是一个位于沿海地区的沙滩海岸线,并且有海浪拍打着岸边。这样的场景在全球许多美丽的海滨地区都可以找到。如果您需要更精确的信息,请提供更多的背景或细节描述。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1723949230,
|
||||
"model": "qwen-vl-plus",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 1279,
|
||||
"completion_tokens": 78
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**文本向量请求示例**
|
||||
|
||||
URL: http://your-domain/v1/embeddings
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "text-embedding-v1",
|
||||
"input": "Hello"
|
||||
}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"index": 0,
|
||||
"embedding": [
|
||||
-1.0437825918197632,
|
||||
5.208984375,
|
||||
3.0483806133270264,
|
||||
-1.7897135019302368,
|
||||
-2.0107421875,
|
||||
...,
|
||||
0.8125,
|
||||
-1.1759847402572632,
|
||||
0.8174641728401184,
|
||||
1.0432943105697632,
|
||||
-0.5885213017463684
|
||||
]
|
||||
}
|
||||
],
|
||||
"model": "text-embedding-v1",
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"total_tokens": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用通义千问配合纯文本上下文信息
|
||||
|
||||
使用通义千问服务,同时配置纯文本上下文信息。
|
||||
@@ -758,6 +944,299 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理360智脑服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: ai360
|
||||
apiTokens:
|
||||
- "YOUR_MINIMAX_API_TOKEN"
|
||||
modelMapping:
|
||||
"gpt-4o": "360gpt-turbo-responsibility-8k"
|
||||
"gpt-4": "360gpt2-pro"
|
||||
"gpt-3.5": "360gpt-turbo"
|
||||
"*": "360gpt-pro"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一个专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好,我是360智脑,一个大型语言模型。我可以帮助回答各种问题、提供信息、进行对话等。有什么可以帮助你的吗?"
|
||||
},
|
||||
"finish_reason": "",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"created": 1724257207,
|
||||
"id": "5e5c94a2-d989-40b5-9965-5b971db941fe",
|
||||
"model": "360gpt-turbo",
|
||||
"object": "",
|
||||
"usage": {
|
||||
"completion_tokens": 33,
|
||||
"prompt_tokens": 24,
|
||||
"total_tokens": 57
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一个专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"context": null
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 Cloudflare Workers AI 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: cloudflare
|
||||
apiTokens:
|
||||
- "YOUR_WORKERS_AI_API_TOKEN"
|
||||
cloudflareAccountId: "YOUR_CLOUDFLARE_ACCOUNT_ID"
|
||||
modelMapping:
|
||||
"*": "@cf/meta/llama-3-8b-instruct"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3.5",
|
||||
"max_tokens": 1024,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "id-1720367803430",
|
||||
"object": "chat.completion",
|
||||
"created": 1720367803,
|
||||
"model": "@cf/meta/llama-3-8b-instruct",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input in a conversational manner. I'm not a human, but a computer program designed to simulate conversation and answer questions to the best of my knowledge. I can be used to generate text on a wide range of topics, from science and history to entertainment and culture.\n\nI'm a large language model, which means I've been trained on a massive dataset of text from the internet and can generate human-like responses. I can understand natural language and respond accordingly, making me suitable for tasks such as:\n\n* Answering questions on various topics\n* Generating text based on a given prompt\n* Translating text from one language to another\n* Summarizing long pieces of text\n* Creating chatbot dialogues\n\nI'm constantly learning and improving, so the more conversations I have with users like you, the better I'll become."
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理Spark服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: spark
|
||||
apiTokens:
|
||||
- "APIKey:APISecret"
|
||||
modelMapping:
|
||||
"gpt-4o": "generalv3.5"
|
||||
"gpt-4": "generalv3"
|
||||
"*": "general"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cha000c23c6@dx190ef0b4b96b8f2532",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员,擅长编程和解决技术问题。有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1721997415,
|
||||
"model": "generalv3.5",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 19,
|
||||
"total_tokens": 29
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 gemini 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: gemini
|
||||
apiTokens:
|
||||
- "YOUR_GEMINI_API_TOKEN"
|
||||
modelMapping:
|
||||
"*": "gemini-pro"
|
||||
geminiSafetySetting:
|
||||
"HARM_CATEGORY_SEXUALLY_EXPLICIT" :"BLOCK_NONE"
|
||||
"HARM_CATEGORY_HATE_SPEECH" :"BLOCK_NONE"
|
||||
"HARM_CATEGORY_HARASSMENT" :"BLOCK_NONE"
|
||||
"HARM_CATEGORY_DANGEROUS_CONTENT" :"BLOCK_NONE"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-b010867c-0d3f-40ba-95fd-4e8030551aeb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am a large multi-modal model, trained by Google. I am designed to provide information and answer questions to the best of my abilities."
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1722756984,
|
||||
"model": "gemini-pro",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 29,
|
||||
"total_tokens": 34
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 DeepL 文本翻译服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: deepl
|
||||
apiTokens:
|
||||
- "YOUR_DEEPL_API_TOKEN"
|
||||
targetLang: "ZH"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
此处 `model` 表示 DeepL 的服务类型,只能填 `Free` 或 `Pro`。`content` 中设置需要翻译的文本;在 `role: system` 的 `content` 中可以包含可能影响翻译但本身不会被翻译的上下文,例如翻译产品名称时,可以将产品描述作为上下文传递,这种额外的上下文可能会提高翻译的质量。
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "Free",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "money"
|
||||
},
|
||||
{
|
||||
"content": "sit by the bank"
|
||||
},
|
||||
{
|
||||
"content": "a bank in China"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": { "name": "EN", "role": "assistant", "content": "坐庄" }
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"message": { "name": "EN", "role": "assistant", "content": "中国银行" }
|
||||
}
|
||||
],
|
||||
"created": 1722747752,
|
||||
"model": "Free",
|
||||
"object": "chat.completion",
|
||||
"usage": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
### Kubernetes 示例
|
||||
|
||||
@@ -50,3 +50,7 @@ func (c *PluginConfig) Complete() error {
|
||||
func (c *PluginConfig) GetProvider() provider.Provider {
|
||||
return c.provider
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetProviderConfig() provider.ProviderConfig {
|
||||
return c.providerConfig
|
||||
}
|
||||
|
||||
@@ -8,19 +8,20 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,14 +4,15 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -20,6 +21,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -21,6 +21,8 @@ const (
|
||||
pluginName = "ai-proxy"
|
||||
|
||||
ctxKeyApiName = "apiKey"
|
||||
|
||||
defaultMaxBodyBytes uint32 = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -61,10 +63,10 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
|
||||
|
||||
rawPath := ctx.Path()
|
||||
path, _ := url.Parse(rawPath)
|
||||
apiName := getApiName(path.Path)
|
||||
apiName := getOpenAiApiName(path.Path)
|
||||
if apiName == "" {
|
||||
log.Debugf("[onHttpRequestHeader] unsupported path: %s", path.Path)
|
||||
_ = util.SendResponse(404, util.MimeTypeTextPlain, "API not found: "+path.Path)
|
||||
_ = util.SendResponse(404, "ai-proxy.unknown_api", util.MimeTypeTextPlain, "API not found: "+path.Path)
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.SetContext(ctxKeyApiName, apiName)
|
||||
@@ -73,18 +75,20 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
|
||||
action, err := handler.OnRequestHeaders(ctx, apiName, log)
|
||||
_, err := handler.OnRequestHeaders(ctx, apiName, log)
|
||||
if err == nil {
|
||||
return action
|
||||
if wrapper.HasRequestBody() {
|
||||
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
|
||||
// Always return types.HeaderStopIteration to support fallback routing,
|
||||
// as long as onHttpRequestBody can be called.
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process request headers: %v", err))
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_headers_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process request headers: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if _, needHandleBody := activeProvider.(provider.RequestBodyHandler); needHandleBody {
|
||||
ctx.DontReadRequestBody()
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -99,18 +103,32 @@ func onHttpRequestBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfig
|
||||
log.Debugf("[onHttpRequestBody] provider=%s", activeProvider.GetProviderType())
|
||||
|
||||
if handler, ok := activeProvider.(provider.RequestBodyHandler); ok {
|
||||
apiName := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
|
||||
newBody, settingErr := pluginConfig.GetProviderConfig().ReplaceByCustomSettings(body)
|
||||
if settingErr != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to rewrite request body by custom settings: %v", settingErr))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
log.Debugf("[onHttpRequestBody] newBody=%s", newBody)
|
||||
body = newBody
|
||||
action, err := handler.OnRequestBody(ctx, apiName, body, log)
|
||||
if err == nil {
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process request body: %v", err))
|
||||
return types.ActionContinue
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process request body: %v", err))
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, pluginConfig config.PluginConfig, log wrapper.Log) types.Action {
|
||||
if !wrapper.IsResponseFromUpstream() {
|
||||
// Response is not coming from the upstream. Let it pass through.
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
activeProvider := pluginConfig.GetProvider()
|
||||
|
||||
if activeProvider == nil {
|
||||
@@ -130,24 +148,18 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, pluginConfig config.PluginCo
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
contentType, err := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
if err != nil {
|
||||
log.Errorf("unable to load content-type header from response: %v", err)
|
||||
}
|
||||
ctx.BufferResponseBody()
|
||||
}
|
||||
|
||||
if handler, ok := activeProvider.(provider.ResponseHeadersHandler); ok {
|
||||
apiName := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
action, err := handler.OnResponseHeaders(ctx, apiName, log)
|
||||
if err == nil {
|
||||
checkStream(&ctx, &log)
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process response headers: %v", err))
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_resp_headers_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process response headers: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
checkStream(&ctx, &log)
|
||||
_, needHandleBody := activeProvider.(provider.ResponseBodyHandler)
|
||||
_, needHandleStreamingBody := activeProvider.(provider.StreamingResponseBodyHandler)
|
||||
if !needHandleBody && !needHandleStreamingBody {
|
||||
@@ -171,7 +183,7 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
|
||||
log.Debugf("isLastChunk=%v chunk: %s", isLastChunk, string(chunk))
|
||||
|
||||
if handler, ok := activeProvider.(provider.StreamingResponseBodyHandler); ok {
|
||||
apiName := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
modifiedChunk, err := handler.OnStreamingResponseBody(ctx, apiName, chunk, isLastChunk, log)
|
||||
if err == nil && modifiedChunk != nil {
|
||||
return modifiedChunk
|
||||
@@ -193,20 +205,33 @@ func onHttpResponseBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfi
|
||||
//log.Debugf("response body: %s", string(body))
|
||||
|
||||
if handler, ok := activeProvider.(provider.ResponseBodyHandler); ok {
|
||||
apiName := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
action, err := handler.OnResponseBody(ctx, apiName, body, log)
|
||||
if err == nil {
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process response body: %v", err))
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_resp_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process response body: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func getApiName(path string) provider.ApiName {
|
||||
func getOpenAiApiName(path string) provider.ApiName {
|
||||
if strings.HasSuffix(path, "/v1/chat/completions") {
|
||||
return provider.ApiNameChatCompletion
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/embeddings") {
|
||||
return provider.ApiNameEmbeddings
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkStream(ctx *wrapper.HttpContext, log *wrapper.Log) {
|
||||
contentType, err := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
if err != nil {
|
||||
log.Errorf("unable to load content-type header from response: %v", err)
|
||||
}
|
||||
(*ctx).BufferResponseBody()
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user