Compare commits

...

19 Commits

Author SHA1 Message Date
Yiiong
74ddbf02f6 feat:add build-and-push-wasm-plugin-image.yaml (#1069)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-08 21:44:58 +08:00
zzjin
60c56a16ab Support CredentialConfig.TLSSecret with namespace. Resolve: #1066 (#1095)
Signed-off-by: zzjin <tczzjin@gmail.com>
2024-07-08 19:49:51 +08:00
Kent Dong
5a2c6835f7 feat: Support embeddings API for Qwen in the ai-proxy plugin (#1079) 2024-07-08 19:37:08 +08:00
Kent Dong
12a5612450 feat: Support model prefix mapping in ai-proxy (#1097) 2024-07-08 19:33:08 +08:00
nohup
b9f5c4d1f2 feat: support Cloudflare Workers AI (#1068)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-07-08 19:27:11 +08:00
Jun
d7bdcbd026 fix priorityClassName missed (#1096) 2024-07-08 19:26:08 +08:00
野生程序员
dd284d1f24 feat: loadBalancerClass (#1071) 2024-07-08 10:58:33 +08:00
jiaomh
a7ee523c98 Update test/README.md (#1098) 2024-07-07 10:06:32 +08:00
Kent Dong
4bbfb131ee feat: Load 3rd-party images from higress image repo (#1067) 2024-07-04 20:14:00 +08:00
Se7en
6fd71f9749 fix: prometheus port (#1076) 2024-07-03 13:46:32 +08:00
pepesi
e0159f501a fix jwt-auth plugin claims_to_headers failed (#1075) 2024-07-03 10:11:17 +08:00
Kent Dong
56226d5052 feat: Create an IngressClass resource in the helm chart (#1072) 2024-07-02 21:22:00 +08:00
pepesi
086a9cc973 fixed ai-statistics plugin statistics error (#1060) 2024-07-02 20:35:12 +08:00
Tao Jikun
e389313aa3 feat: update doc for running Ingress API conformance tests (#1065)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-06-27 14:55:10 +08:00
澄潭
f64c601264 compatiable with openai sdk (#1061)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2024-06-27 09:30:52 +08:00
co63oc
9c6ea109f8 Fix typos (#1053) 2024-06-26 19:47:39 +08:00
yy
4ca2d23404 feat: helm charts support installing gateway in daemonset mod. (#1054) 2024-06-26 19:47:20 +08:00
co63oc
0ce52de59b Fix typos (#1050) 2024-06-22 16:22:03 +08:00
澄潭
81e459da01 Update Makefile.core.mk 2024-06-19 17:40:22 +08:00
40 changed files with 1482 additions and 225 deletions

View File

@@ -0,0 +1,102 @@
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-image:
runs-on: ubuntu-latest
environment:
name: image-registry-msg
env:
IMAGE_REGISTRY_SERVICE: ${{ vars.IMAGE_REGISTRY_SERVICE || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
IMAGE_REPOSITORY: ${{ vars.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正常解析
command="
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' ./main.go
tar czvf plugin.tar.gz plugin.wasm
echo ${{ secrets.REGISTRY_PASSWORD }} | oras login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ env.IMAGE_REGISTRY_SERVICE }}
oras push ${IMAGE_REGISTRY_SERVICE}/${IMAGE_REPOSITORY}:${VERSION} ${push_command}
"
docker exec builder bash -c "$command"

View File

@@ -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-63539ca
ISTIO_LATEST_IMAGE_TAG ?= sha-63539ca
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'

341
get_helm.sh Executable file
View 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

View File

@@ -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"]
@@ -66,7 +66,7 @@ rules:
- 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"]

View File

@@ -26,6 +26,9 @@ 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:

View 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 }}

View File

@@ -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 }}
@@ -202,6 +206,9 @@ spec:
value: {{ $val | quote }}
{{- end }}
ports:
- containerPort: 15020
protocol: TCP
name: istio-prom
- containerPort: 15090
protocol: TCP
name: http-envoy-prom
@@ -241,7 +248,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
@@ -340,3 +347,4 @@ spec:
path: /opt/plugins
type: Directory
{{- end }}
{{- end }}

View File

@@ -0,0 +1,6 @@
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: {{ .Values.global.ingressClass }}
spec:
controller: higress.io/higress-controller

View File

@@ -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 }}

View File

@@ -343,7 +343,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 +396,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 +471,7 @@ gateway:
targetPort: 443
annotations: {}
loadBalancerIP: ""
loadBalancerClass: ""
loadBalancerSourceRanges: []
externalTrafficPolicy: ""

View File

@@ -86,6 +86,17 @@ 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 {
@@ -111,14 +122,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)
}
}

View File

@@ -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)
})
}
}

View File

@@ -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,20 @@ 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{})
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 +63,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,18 +71,7 @@ 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")
@@ -97,8 +88,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,

View File

@@ -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())

View File

@@ -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")

View File

@@ -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

View File

@@ -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 {

View File

@@ -918,7 +918,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 +960,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 +987,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 +1042,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 +1054,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 +1160,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 +1293,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{})

View File

@@ -433,6 +433,11 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
// If there is no matching secret, try to get it from configmap.
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
secretNamespace = c.options.SystemNamespace
namespace, secret := cert.ParseTLSSecret(secretName)
if namespace != "" {
secretNamespace = namespace
secretName = secret
}
}
}
}
@@ -441,6 +446,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 == "" {

View File

@@ -419,6 +419,11 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
// If there is no matching secret, try to get it from configmap.
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
secretNamespace = c.options.SystemNamespace
namespace, secret := cert.ParseTLSSecret(secretName)
if namespace != "" {
secretNamespace = namespace
secretName = secret
}
}
}
}
@@ -427,6 +432,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
}
}
}

View File

@@ -19,14 +19,14 @@ 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 对话上下文信息 |
`context`的配置字段说明如下:
@@ -131,6 +131,15 @@ 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) |
## 用法示例
### 使用 OpenAI 协议代理 Azure OpenAI 服务
@@ -246,25 +255,72 @@ provider:
'gpt-3': "qwen-turbo"
'gpt-35-turbo': "qwen-plus"
'gpt-4-turbo': "qwen-max"
'gpt-4-*': "qwen-max"
'text-embedding-v1': 'text-embedding-v1'
'*': "qwen-turbo"
```
**AI 对话请求示例**
URL: http://your-domain/v1/chat/completions
请求体:
```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
}
}
```
**请求示例**
URL: http://your-domain/v1/embeddings
示例请求内容:
```json
{
"model": "gpt-3",
"messages": [
{
"role": "user",
"content": "你好,你是谁?"
}
],
"temperature": 0.3
"model": "text-embedding-v1",
"input": [
"Hello world!"
]
}
```
**响应示例**
示例响应内容:
```json
{
@@ -758,6 +814,57 @@ provider:
}
```
### 使用 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"
}
]
}
```
## 完整配置示例
### Kubernetes 示例

View File

@@ -61,7 +61,7 @@ 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)
@@ -77,7 +77,7 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
if err == nil {
return action
}
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process request headers: %v", err))
_ = util.SendResponse(500, util.MimeTypeTextPlain, fmt.Sprintf("failed to process request headers: %v", err))
return types.ActionContinue
}
@@ -104,7 +104,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfig
if err == nil {
return action
}
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process request body: %v", err))
_ = util.SendResponse(500, util.MimeTypeTextPlain, fmt.Sprintf("failed to process request body: %v", err))
return types.ActionContinue
}
return types.ActionContinue
@@ -144,7 +144,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, pluginConfig config.PluginCo
if err == nil {
return action
}
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process response headers: %v", err))
_ = util.SendResponse(500, util.MimeTypeTextPlain, fmt.Sprintf("failed to process response headers: %v", err))
return types.ActionContinue
}
@@ -198,15 +198,18 @@ func onHttpResponseBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfi
if err == nil {
return action
}
_ = util.SendResponse(404, util.MimeTypeTextPlain, fmt.Sprintf("failed to process response body: %v", err))
_ = util.SendResponse(500, 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 ""
}

View File

@@ -302,7 +302,7 @@ func (b *baiduProvider) responseBaidu2OpenAI(ctx wrapper.HttpContext, response *
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: []chatCompletionChoice{choice},
Usage: chatCompletionUsage{
Usage: usage{
PromptTokens: response.Usage.PromptTokens,
CompletionTokens: response.Usage.CompletionTokens,
TotalTokens: response.Usage.TotalTokens,
@@ -325,7 +325,7 @@ func (b *baiduProvider) streamResponseBaidu2OpenAI(ctx wrapper.HttpContext, resp
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: []chatCompletionChoice{choice},
Usage: chatCompletionUsage{
Usage: usage{
PromptTokens: response.Usage.PromptTokens,
CompletionTokens: response.Usage.CompletionTokens,
TotalTokens: response.Usage.TotalTokens,

View File

@@ -296,7 +296,7 @@ func (c *claudeProvider) responseClaude2OpenAI(ctx wrapper.HttpContext, origResp
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: []chatCompletionChoice{choice},
Usage: chatCompletionUsage{
Usage: usage{
PromptTokens: origResponse.Usage.InputTokens,
CompletionTokens: origResponse.Usage.OutputTokens,
TotalTokens: origResponse.Usage.InputTokens + origResponse.Usage.OutputTokens,

View File

@@ -0,0 +1,108 @@
package provider
import (
"errors"
"fmt"
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"strings"
)
const (
cloudflareDomain = "api.cloudflare.com"
// https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/
cloudflareChatCompletionPath = "/client/v4/accounts/{account_id}/ai/v1/chat/completions"
)
type cloudflareProviderInitializer struct {
}
func (c *cloudflareProviderInitializer) ValidateConfig(config ProviderConfig) error {
return nil
}
func (c *cloudflareProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
return &cloudflareProvider{
config: config,
contextCache: createContextCache(&config),
}, nil
}
type cloudflareProvider struct {
config ProviderConfig
contextCache *contextCache
}
func (c *cloudflareProvider) GetProviderType() string {
return providerTypeCloudflare
}
func (c *cloudflareProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
if apiName != ApiNameChatCompletion {
return types.ActionContinue, errUnsupportedApiName
}
_ = util.OverwriteRequestPath(strings.Replace(cloudflareChatCompletionPath, "{account_id}", c.config.cloudflareAccountId, 1))
_ = util.OverwriteRequestHost(cloudflareDomain)
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", "Bearer "+c.config.GetRandomToken())
if c.config.context == nil && c.config.protocol == protocolOriginal {
ctx.DontReadRequestBody()
}
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
return types.ActionContinue, nil
}
func (c *cloudflareProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
if apiName != ApiNameChatCompletion {
return types.ActionContinue, errUnsupportedApiName
}
request := &chatCompletionRequest{}
if err := decodeChatCompletionRequest(body, request); err != nil {
return types.ActionContinue, err
}
model := request.Model
if model == "" {
return types.ActionContinue, errors.New("missing model in chat completion request")
}
ctx.SetContext(ctxKeyOriginalRequestModel, model)
mappedModel := getMappedModel(model, c.config.modelMapping, log)
if mappedModel == "" {
return types.ActionContinue, errors.New("model becomes empty after applying the configured mapping")
}
request.Model = mappedModel
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
streaming := request.Stream
if streaming {
_ = proxywasm.ReplaceHttpRequestHeader("Accept", "text/event-stream")
}
if c.contextCache == nil {
if err := replaceJsonRequestBody(request, log); err != nil {
_ = util.SendResponse(500, util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
}
return types.ActionContinue, nil
}
err := c.contextCache.GetContent(func(content string, err error) {
defer func() {
_ = proxywasm.ResumeHttpRequest()
}()
if err != nil {
log.Errorf("failed to load context file: %v", err)
_ = util.SendResponse(500, util.MimeTypeTextPlain, fmt.Sprintf("failed to load context file: %v", err))
}
insertContextMessage(request, content)
if err := replaceJsonRequestBody(request, log); err != nil {
_ = util.SendResponse(500, util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
}
}, log)
if err == nil {
return types.ActionPause, nil
}
return types.ActionContinue, err
}

View File

@@ -354,7 +354,7 @@ func (m *hunyuanProvider) convertChunkFromHunyuanToOpenAI(ctx wrapper.HttpContex
Model: ctx.GetContext(ctxKeyFinalRequestModel).(string),
SystemFingerprint: "",
Object: objectChatCompletionChunk,
Usage: chatCompletionUsage{
Usage: usage{
PromptTokens: hunyuanFormattedChunk.Usage.PromptTokens,
CompletionTokens: hunyuanFormattedChunk.Usage.CompletionTokens,
TotalTokens: hunyuanFormattedChunk.Usage.TotalTokens,
@@ -474,7 +474,7 @@ func (m *hunyuanProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, h
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: choices,
Usage: chatCompletionUsage{
Usage: usage{
PromptTokens: hunyuanResponse.Response.Usage.PromptTokens,
CompletionTokens: hunyuanResponse.Response.Usage.CompletionTokens,
TotalTokens: hunyuanResponse.Response.Usage.TotalTokens,

View File

@@ -461,7 +461,7 @@ func (m *minimaxProvider) responseV2ToOpenAI(response *minimaxChatCompletionV2Re
Created: response.Created,
Model: response.Model,
Choices: choices,
Usage: chatCompletionUsage{
Usage: usage{
TotalTokens: int(response.Usage.TotalTokens),
},
}

View File

@@ -60,7 +60,7 @@ type chatCompletionResponse struct {
Model string `json:"model,omitempty"`
SystemFingerprint string `json:"system_fingerprint,omitempty"`
Object string `json:"object,omitempty"`
Usage chatCompletionUsage `json:"usage,omitempty"`
Usage usage `json:"usage,omitempty"`
}
type chatCompletionChoice struct {
@@ -70,7 +70,7 @@ type chatCompletionChoice struct {
FinishReason string `json:"finish_reason,omitempty"`
}
type chatCompletionUsage struct {
type usage struct {
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
@@ -140,3 +140,24 @@ func (e *streamEvent) setValue(key, value string) {
}
}
}
type embeddingsRequest struct {
Input interface{} `json:"input"`
Model string `json:"model"`
EncodingFormat string `json:"encoding_format,omitempty"`
Dimensions int `json:"dimensions,omitempty"`
User string `json:"user,omitempty"`
}
type embeddingsResponse struct {
Object string `json:"object"`
Data []embedding `json:"data"`
Model string `json:"model"`
Usage usage `json:"usage"`
}
type embedding struct {
Object string `json:"object"`
Index int `json:"index"`
Embedding []float64 `json:"embedding"`
}

View File

@@ -14,6 +14,7 @@ import (
const (
openaiDomain = "api.openai.com"
openaiChatCompletionPath = "/v1/chat/completions"
openaiEmbeddingsPath = "/v1/chat/embeddings"
)
type openaiProviderInitializer struct {
@@ -40,14 +41,19 @@ func (m *openaiProvider) GetProviderType() string {
}
func (m *openaiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
if apiName != ApiNameChatCompletion {
return types.ActionContinue, errUnsupportedApiName
skipRequestBody := true
switch apiName {
case ApiNameChatCompletion:
_ = util.OverwriteRequestPath(openaiChatCompletionPath)
skipRequestBody = m.contextCache == nil
break
case ApiNameEmbeddings:
_ = util.OverwriteRequestPath(openaiEmbeddingsPath)
break
}
_ = util.OverwriteRequestPath(openaiChatCompletionPath)
_ = util.OverwriteRequestHost(openaiDomain)
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", "Bearer "+m.config.GetRandomToken())
if m.contextCache == nil {
if skipRequestBody {
ctx.DontReadRequestBody()
} else {
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
@@ -58,7 +64,8 @@ func (m *openaiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiNa
func (m *openaiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
if apiName != ApiNameChatCompletion {
return types.ActionContinue, errUnsupportedApiName
// We don't need to process the request body for other APIs.
return types.ActionContinue, nil
}
if m.contextCache == nil {
return types.ActionContinue, nil

View File

@@ -3,6 +3,7 @@ package provider
import (
"errors"
"math/rand"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
@@ -14,22 +15,24 @@ type Pointcut string
const (
ApiNameChatCompletion ApiName = "chatCompletion"
ApiNameEmbeddings ApiName = "embeddings"
providerTypeMoonshot = "moonshot"
providerTypeAzure = "azure"
providerTypeQwen = "qwen"
providerTypeOpenAI = "openai"
providerTypeGroq = "groq"
providerTypeBaichuan = "baichuan"
providerTypeYi = "yi"
providerTypeDeepSeek = "deepseek"
providerTypeZhipuAi = "zhipuai"
providerTypeOllama = "ollama"
providerTypeClaude = "claude"
providerTypeBaidu = "baidu"
providerTypeHunyuan = "hunyuan"
providerTypeStepfun = "stepfun"
providerTypeMinimax = "minimax"
providerTypeMoonshot = "moonshot"
providerTypeAzure = "azure"
providerTypeQwen = "qwen"
providerTypeOpenAI = "openai"
providerTypeGroq = "groq"
providerTypeBaichuan = "baichuan"
providerTypeYi = "yi"
providerTypeDeepSeek = "deepseek"
providerTypeZhipuAi = "zhipuai"
providerTypeOllama = "ollama"
providerTypeClaude = "claude"
providerTypeBaidu = "baidu"
providerTypeHunyuan = "hunyuan"
providerTypeStepfun = "stepfun"
providerTypeMinimax = "minimax"
providerTypeCloudflare = "cloudflare"
protocolOpenAI = "openai"
protocolOriginal = "original"
@@ -65,21 +68,22 @@ var (
errUnsupportedApiName = errors.New("unsupported API name")
providerInitializers = map[string]providerInitializer{
providerTypeMoonshot: &moonshotProviderInitializer{},
providerTypeAzure: &azureProviderInitializer{},
providerTypeQwen: &qwenProviderInitializer{},
providerTypeOpenAI: &openaiProviderInitializer{},
providerTypeGroq: &groqProviderInitializer{},
providerTypeBaichuan: &baichuanProviderInitializer{},
providerTypeYi: &yiProviderInitializer{},
providerTypeDeepSeek: &deepseekProviderInitializer{},
providerTypeZhipuAi: &zhipuAiProviderInitializer{},
providerTypeOllama: &ollamaProviderInitializer{},
providerTypeClaude: &claudeProviderInitializer{},
providerTypeBaidu: &baiduProviderInitializer{},
providerTypeHunyuan: &hunyuanProviderInitializer{},
providerTypeStepfun: &stepfunProviderInitializer{},
providerTypeMinimax: &minimaxProviderInitializer{},
providerTypeMoonshot: &moonshotProviderInitializer{},
providerTypeAzure: &azureProviderInitializer{},
providerTypeQwen: &qwenProviderInitializer{},
providerTypeOpenAI: &openaiProviderInitializer{},
providerTypeGroq: &groqProviderInitializer{},
providerTypeBaichuan: &baichuanProviderInitializer{},
providerTypeYi: &yiProviderInitializer{},
providerTypeDeepSeek: &deepseekProviderInitializer{},
providerTypeZhipuAi: &zhipuAiProviderInitializer{},
providerTypeOllama: &ollamaProviderInitializer{},
providerTypeClaude: &claudeProviderInitializer{},
providerTypeBaidu: &baiduProviderInitializer{},
providerTypeHunyuan: &hunyuanProviderInitializer{},
providerTypeStepfun: &stepfunProviderInitializer{},
providerTypeMinimax: &minimaxProviderInitializer{},
providerTypeCloudflare: &cloudflareProviderInitializer{},
}
)
@@ -156,6 +160,9 @@ type ProviderConfig struct {
// @Title zh-CN 版本
// @Description zh-CN 请求AI服务的版本目前仅适用于Claude AI服务
claudeVersion string `required:"false" yaml:"version" json:"version"`
// @Title zh-CN Cloudflare Account ID
// @Description zh-CN 仅适用于 Cloudflare Workers AI 服务。参考https://developers.cloudflare.com/workers-ai/get-started/rest-api/#2-run-a-model-via-api
cloudflareAccountId string `required:"false" yaml:"cloudflareAccountId" json:"cloudflareAccountId"`
}
func (c *ProviderConfig) FromJson(json gjson.Result) {
@@ -194,6 +201,7 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
c.hunyuanAuthId = json.Get("hunyuanAuthId").String()
c.hunyuanAuthKey = json.Get("hunyuanAuthKey").String()
c.minimaxGroupId = json.Get("minimaxGroupId").String()
c.cloudflareAccountId = json.Get("cloudflareAccountId").String()
}
func (c *ProviderConfig) Validate() error {
@@ -247,16 +255,38 @@ func CreateProvider(pc ProviderConfig) (Provider, error) {
}
func getMappedModel(model string, modelMapping map[string]string, log wrapper.Log) string {
if modelMapping == nil || len(modelMapping) == 0 {
return model
}
if v, ok := modelMapping[model]; ok && len(v) != 0 {
log.Debugf("model %s is mapped to %s explictly", model, v)
return v
}
if v, ok := modelMapping[wildcard]; ok {
log.Debugf("model %s is mapped to %s via wildcard", model, v)
return v
mappedModel := doGetMappedModel(model, modelMapping, log)
if len(mappedModel) != 0 {
return mappedModel
}
return model
}
func doGetMappedModel(model string, modelMapping map[string]string, log wrapper.Log) string {
if modelMapping == nil || len(modelMapping) == 0 {
return ""
}
if v, ok := modelMapping[model]; ok {
log.Debugf("model [%s] is mapped to [%s] explictly", model, v)
return v
}
for k, v := range modelMapping {
if k == wildcard || !strings.HasSuffix(k, wildcard) {
continue
}
k = strings.TrimSuffix(k, wildcard)
if strings.HasPrefix(model, k) {
log.Debugf("model [%s] is mapped to [%s] via prefix [%s]", model, v, k)
return v
}
}
if v, ok := modelMapping[wildcard]; ok {
log.Debugf("model [%s] is mapped to [%s] via wildcard", model, v)
return v
}
return ""
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math"
"reflect"
"strings"
"time"
@@ -21,6 +22,7 @@ const (
qwenDomain = "dashscope.aliyuncs.com"
qwenChatCompletionPath = "/api/v1/services/aigc/text-generation/generation"
qwenTextEmbeddingPath = "/api/v1/services/embeddings/text-embedding/text-embedding"
qwenTopPMin = 0.000001
qwenTopPMax = 0.999999
@@ -58,14 +60,19 @@ func (m *qwenProvider) GetProviderType() string {
}
func (m *qwenProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
if apiName != ApiNameChatCompletion {
needRequestBody := false
if apiName == ApiNameChatCompletion {
_ = util.OverwriteRequestPath(qwenChatCompletionPath)
needRequestBody = m.config.context != nil
} else if apiName == ApiNameEmbeddings {
_ = util.OverwriteRequestPath(qwenTextEmbeddingPath)
} else {
return types.ActionContinue, errUnsupportedApiName
}
_ = util.OverwriteRequestPath(qwenChatCompletionPath)
_ = util.OverwriteRequestHost(qwenDomain)
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", "Bearer "+m.config.GetRandomToken())
if m.config.protocol == protocolOriginal && m.config.context == nil {
if m.config.protocol == protocolOriginal && !needRequestBody {
ctx.DontReadRequestBody()
return types.ActionContinue, nil
}
@@ -78,10 +85,16 @@ func (m *qwenProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName
}
func (m *qwenProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
if apiName != ApiNameChatCompletion {
return types.ActionContinue, errUnsupportedApiName
if apiName == ApiNameChatCompletion {
return m.onChatCompletionRequestBody(ctx, body, log)
}
if apiName == ApiNameEmbeddings {
return m.onEmbeddingsRequestBody(ctx, body, log)
}
return types.ActionContinue, errUnsupportedApiName
}
func (m *qwenProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
if m.config.protocol == protocolOriginal {
if m.config.context == nil {
return types.ActionContinue, nil
@@ -169,6 +182,33 @@ func (m *qwenProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, b
return types.ActionContinue, err
}
func (m *qwenProvider) onEmbeddingsRequestBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
request := &embeddingsRequest{}
if err := json.Unmarshal(body, request); err != nil {
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
}
log.Debugf("=== embeddings request: %v", request)
model := request.Model
if model == "" {
return types.ActionContinue, errors.New("missing model in the request")
}
ctx.SetContext(ctxKeyOriginalRequestModel, model)
mappedModel := getMappedModel(model, m.config.modelMapping, log)
if mappedModel == "" {
return types.ActionContinue, errors.New("model becomes empty after applying the configured mapping")
}
request.Model = mappedModel
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
if qwenRequest, err := m.buildQwenTextEmbeddingRequest(request); err == nil {
return types.ActionContinue, replaceJsonRequestBody(qwenRequest, log)
} else {
return types.ActionContinue, err
}
}
func (m *qwenProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
if m.config.protocol == protocolOriginal {
ctx.DontReadResponseBody()
@@ -180,6 +220,10 @@ func (m *qwenProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiNam
}
func (m *qwenProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
if name != ApiNameChatCompletion {
return chunk, nil
}
receivedBody := chunk
if bufferedStreamingBody, has := ctx.GetContext(ctxKeyStreamingBody).([]byte); has {
receivedBody = append(bufferedStreamingBody, chunk...)
@@ -264,6 +308,16 @@ func (m *qwenProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name Api
}
func (m *qwenProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
if apiName == ApiNameChatCompletion {
return m.onChatCompletionResponseBody(ctx, body, log)
}
if apiName == ApiNameEmbeddings {
return m.onEmbeddingsResponseBody(ctx, body, log)
}
return types.ActionContinue, errUnsupportedApiName
}
func (m *qwenProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
qwenResponse := &qwenTextGenResponse{}
if err := json.Unmarshal(body, qwenResponse); err != nil {
return types.ActionContinue, fmt.Errorf("unable to unmarshal Qwen response: %v", err)
@@ -272,6 +326,15 @@ func (m *qwenProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName,
return types.ActionContinue, replaceJsonResponseBody(response, log)
}
func (m *qwenProvider) onEmbeddingsResponseBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
qwenResponse := &qwenTextEmbeddingResponse{}
if err := json.Unmarshal(body, qwenResponse); err != nil {
return types.ActionContinue, fmt.Errorf("unable to unmarshal Qwen response: %v", err)
}
response := m.buildEmbeddingsResponse(ctx, qwenResponse)
return types.ActionContinue, replaceJsonResponseBody(response, log)
}
func (m *qwenProvider) buildQwenTextGenerationRequest(origRequest *chatCompletionRequest, streaming bool) *qwenTextGenRequest {
messages := make([]qwenMessage, 0, len(origRequest.Messages))
for i := range origRequest.Messages {
@@ -328,7 +391,7 @@ func (m *qwenProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, qwen
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: choices,
Usage: chatCompletionUsage{
Usage: usage{
PromptTokens: qwenResponse.Usage.InputTokens,
CompletionTokens: qwenResponse.Usage.OutputTokens,
TotalTokens: qwenResponse.Usage.TotalTokens,
@@ -396,10 +459,11 @@ func (m *qwenProvider) buildChatCompletionStreamingResponse(ctx wrapper.HttpCont
if finished {
finishResponse := *&baseMessage
finishResponse.Choices = append(finishResponse.Choices, chatCompletionChoice{FinishReason: qwenChoice.FinishReason})
finishResponse.Choices = append(finishResponse.Choices, chatCompletionChoice{Delta: &chatMessage{}, FinishReason: qwenChoice.FinishReason})
usageResponse := *&baseMessage
usageResponse.Usage = chatCompletionUsage{
usageResponse.Choices = []chatCompletionChoice{{Delta: &chatMessage{}}}
usageResponse.Usage = usage{
PromptTokens: qwenResponse.Usage.InputTokens,
CompletionTokens: qwenResponse.Usage.OutputTokens,
TotalTokens: qwenResponse.Usage.TotalTokens,
@@ -484,6 +548,50 @@ func (m *qwenProvider) appendStreamEvent(responseBuilder *strings.Builder, event
responseBuilder.WriteString("\n\n")
}
func (m *qwenProvider) buildQwenTextEmbeddingRequest(request *embeddingsRequest) (*qwenTextEmbeddingRequest, error) {
var texts []string
if str, isString := request.Input.(string); isString {
texts = []string{str}
} else if strs, isArray := request.Input.([]interface{}); isArray {
texts = make([]string, 0, len(strs))
for _, item := range strs {
if str, isString := item.(string); isString {
texts = append(texts, str)
} else {
return nil, errors.New("unsupported input type in array: " + reflect.TypeOf(item).String())
}
}
} else {
return nil, errors.New("unsupported input type: " + reflect.TypeOf(request.Input).String())
}
return &qwenTextEmbeddingRequest{
Model: request.Model,
Input: qwenTextEmbeddingInput{
Texts: texts,
},
}, nil
}
func (m *qwenProvider) buildEmbeddingsResponse(ctx wrapper.HttpContext, qwenResponse *qwenTextEmbeddingResponse) *embeddingsResponse {
data := make([]embedding, 0, len(qwenResponse.Output.Embeddings))
for _, qwenEmbedding := range qwenResponse.Output.Embeddings {
data = append(data, embedding{
Object: "embedding",
Index: qwenEmbedding.TextIndex,
Embedding: qwenEmbedding.Embedding,
})
}
return &embeddingsResponse{
Object: "list",
Data: data,
Model: ctx.GetContext(ctxKeyFinalRequestModel).(string),
Usage: usage{
PromptTokens: qwenResponse.Usage.TotalTokens,
TotalTokens: qwenResponse.Usage.TotalTokens,
},
}
}
type qwenTextGenRequest struct {
Model string `json:"model"`
Input qwenTextGenInput `json:"input"`
@@ -510,7 +618,7 @@ type qwenTextGenParameters struct {
type qwenTextGenResponse struct {
RequestId string `json:"request_id"`
Output qwenTextGenOutput `json:"output"`
Usage qwenTextGenUsage `json:"usage"`
Usage qwenUsage `json:"usage"`
}
type qwenTextGenOutput struct {
@@ -523,7 +631,7 @@ type qwenTextGenChoice struct {
Message qwenMessage `json:"message"`
}
type qwenTextGenUsage struct {
type qwenUsage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalTokens int `json:"total_tokens"`
@@ -536,6 +644,36 @@ type qwenMessage struct {
ToolCalls []toolCall `json:"tool_calls,omitempty"`
}
type qwenTextEmbeddingRequest struct {
Model string `json:"model"`
Input qwenTextEmbeddingInput `json:"input"`
Parameters qwenTextEmbeddingParameters `json:"parameters,omitempty"`
}
type qwenTextEmbeddingInput struct {
Texts []string `json:"texts"`
}
type qwenTextEmbeddingParameters struct {
TextType string `json:"text_type,omitempty"`
}
type qwenTextEmbeddingResponse struct {
RequestId string `json:"request_id"`
Output qwenTextEmbeddingOutput `json:"output"`
Usage qwenUsage `json:"usage"`
}
type qwenTextEmbeddingOutput struct {
RequestId string `json:"request_id"`
Embeddings []qwenTextEmbeddings `json:"embeddings"`
}
type qwenTextEmbeddings struct {
TextIndex int `json:"text_index"`
Embedding []float64 `json:"embedding"`
}
func qwenMessageToChatMessage(qwenMessage qwenMessage) chatMessage {
return chatMessage{
Name: qwenMessage.Name,

View File

@@ -1,6 +1,7 @@
package main
import (
"bytes"
"fmt"
"strings"
@@ -52,79 +53,66 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, l
return types.ActionContinue
}
func getLastChunk(data []byte) []byte {
chunks := strings.Split(strings.TrimSpace(string(data)), "\n\n")
length := len(chunks)
if length < 2 {
func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, data []byte, endOfStream bool, log wrapper.Log) []byte {
model, inputToken, outputToken, ok := getUsage(data)
if !ok {
return data
}
// ai-proxy append extra usage chunk
return []byte(chunks[length-1])
}
func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, data []byte, endOfStream bool, log wrapper.Log) []byte {
lastChunk := getLastChunk(data)
modelObj := gjson.GetBytes(lastChunk, "model")
inputTokenObj := gjson.GetBytes(lastChunk, "usage.prompt_tokens")
outputTokenObj := gjson.GetBytes(lastChunk, "usage.completion_tokens")
if modelObj.Exists() && inputTokenObj.Exists() && outputTokenObj.Exists() {
ctx.SetContext("model", modelObj.String())
ctx.SetContext("input_token", inputTokenObj.Int())
ctx.SetContext("output_token", outputTokenObj.Int())
}
if endOfStream {
var route, cluster string
if raw, err := proxywasm.GetProperty([]string{"route_name"}); err == nil {
route = string(raw)
}
if raw, err := proxywasm.GetProperty([]string{"cluster_name"}); err == nil {
cluster = string(raw)
}
model, ok := ctx.GetContext("model").(string)
if !ok {
log.Error("Get model failed!")
return data
}
inputToken, ok := ctx.GetContext("input_token").(int64)
if !ok {
log.Error("Get input_token failed!")
return data
}
outputToken, ok := ctx.GetContext("output_token").(int64)
if !ok {
log.Error("Get output_token failed!")
return data
}
config.incrementCounter("route."+route+".upstream."+cluster+".model."+model+".input_token", uint64(inputToken), log)
config.incrementCounter("route."+route+".upstream."+cluster+".model."+model+".output_token", uint64(outputToken), log)
proxywasm.SetProperty([]string{"model"}, []byte(model))
proxywasm.SetProperty([]string{"input_token"}, []byte(fmt.Sprint(inputToken)))
proxywasm.SetProperty([]string{"output_token"}, []byte(fmt.Sprint(outputToken)))
}
setFilterStateData(model, inputToken, outputToken, log)
incrementCounter(config, model, inputToken, outputToken, log)
return data
}
func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body []byte, log wrapper.Log) types.Action {
modeObj := gjson.GetBytes(body, "model")
inputTokenObj := gjson.GetBytes(body, "usage.prompt_tokens")
outputTokenObj := gjson.GetBytes(body, "usage.completion_tokens")
if !modeObj.Exists() {
log.Error("Get model failed")
model, inputToken, outputToken, ok := getUsage(body)
if !ok {
return types.ActionContinue
}
if !inputTokenObj.Exists() {
log.Error("Get input_token failed")
return types.ActionContinue
setFilterStateData(model, inputToken, outputToken, log)
incrementCounter(config, model, inputToken, outputToken, log)
return types.ActionContinue
}
func getUsage(data []byte) (model string, inputTokenUsage int64, outputTokenUsage int64, ok bool) {
chunks := bytes.Split(bytes.TrimSpace(data), []byte("\n\n"))
for _, chunk := range chunks {
// the feature strings are used to identify the usage data, like:
// {"model":"gpt2","usage":{"prompt_tokens":1,"completion_tokens":1}}
if !bytes.Contains(chunk, []byte("prompt_tokens")) {
continue
}
if !bytes.Contains(chunk, []byte("completion_tokens")) {
continue
}
modelObj := gjson.GetBytes(chunk, "model")
inputTokenObj := gjson.GetBytes(chunk, "usage.prompt_tokens")
outputTokenObj := gjson.GetBytes(chunk, "usage.completion_tokens")
if modelObj.Exists() && inputTokenObj.Exists() && outputTokenObj.Exists() {
model = modelObj.String()
inputTokenUsage = inputTokenObj.Int()
outputTokenUsage = outputTokenObj.Int()
ok = true
return
}
}
if !outputTokenObj.Exists() {
log.Error("Get output_token failed")
return types.ActionContinue
return
}
// setFilterData sets the input_token and output_token in the filter state.
// ai-token-ratelimit will use these values to calculate the total token usage.
func setFilterStateData(model string, inputToken int64, outputToken int64, log wrapper.Log) {
if e := proxywasm.SetProperty([]string{"model"}, []byte(model)); e != nil {
log.Errorf("failed to set model in filter state: %v", e)
}
model := modeObj.String()
inputToken := inputTokenObj.Int()
outputToken := outputTokenObj.Int()
if e := proxywasm.SetProperty([]string{"input_token"}, []byte(fmt.Sprintf("%d", inputToken))); e != nil {
log.Errorf("failed to set input_token in filter state: %v", e)
}
if e := proxywasm.SetProperty([]string{"output_token"}, []byte(fmt.Sprintf("%d", outputToken))); e != nil {
log.Errorf("failed to set output_token in filter state: %v", e)
}
}
func incrementCounter(config AIStatisticsConfig, model string, inputToken int64, outputToken int64, log wrapper.Log) {
var route, cluster string
if raw, err := proxywasm.GetProperty([]string{"route_name"}); err == nil {
route = string(raw)
@@ -134,10 +122,4 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body
}
config.incrementCounter("route."+route+".upstream."+cluster+".model."+model+".input_token", uint64(inputToken), log)
config.incrementCounter("route."+route+".upstream."+cluster+".model."+model+".output_token", uint64(outputToken), log)
proxywasm.SetProperty([]string{"model"}, []byte(model))
proxywasm.SetProperty([]string{"input_token"}, []byte(fmt.Sprint(inputToken)))
proxywasm.SetProperty([]string{"output_token"}, []byte(fmt.Sprint(outputToken)))
return types.ActionContinue
}

View File

@@ -134,6 +134,7 @@ func consumerVerify(consumer *cfg.Consumer, verifyTime time.Time, header HeaderP
denied: deniedJWTVerificationFails,
}
}
token.UnsafeClaimsWithoutVerification(&rawClaims)
if out.Issuer != consumer.Issuer {
return &ErrDenied{

View File

@@ -31,7 +31,7 @@ import (
)
const (
ConuslHealthPassing = "passing"
ConsulHealthPassing = "passing"
DefaultRefreshInterval = time.Second * 30
DefaultRefreshIntervalLimit = time.Second * 10
)
@@ -315,7 +315,7 @@ func (w *watcher) generateServiceEntry(host string, services []*consulapi.Servic
for _, service := range services {
// service status: maintenance > critical > warning > passing
if service.Checks.AggregatedStatus() != ConuslHealthPassing {
if service.Checks.AggregatedStatus() != ConsulHealthPassing {
continue
}

View File

@@ -42,7 +42,7 @@ func NewEurekaHttpClient(config EurekaHttpConfig) EurekaHttpClient {
type EurekaHttpConfig struct {
BaseUrl string
ConnectTimeoutSeconds int // default 30
PollInterval int //default 30
PollInterval int // default 30
Retries int // default 3
RetryDelayTime int // default 100ms
EnableDelta bool
@@ -101,7 +101,7 @@ func (e *eurekaHttpClient) ScheduleAppUpdates(name string, stop <-chan struct{})
func (e *eurekaHttpClient) GetDelta() (*Applications, error) {
if !e.EnableDelta {
return nil, fmt.Errorf("failed to get DeltaAppliation, enableDelta is false")
return nil, fmt.Errorf("failed to get DeltaApplication, enableDelta is false")
}
return e.getApplications("/apps/delta")
}
@@ -119,7 +119,7 @@ func (c *eurekaHttpClient) getApplications(path string) (*Applications, error) {
var rj fargo.GetAppsResponseJson
if err = json.Unmarshal(res, &rj); err != nil {
log.Errorf("Failed to unmarshal response body to fargo.GetAppResponseJosn, error: %v", err)
log.Errorf("Failed to unmarshal response body to fargo.GetAppResponseJson, error: %v", err)
return nil, err
}

View File

@@ -30,7 +30,7 @@ type Cache interface {
UpdateServiceEntryWrapper(service string, data *ServiceEntryWrapper)
DeleteServiceEntryWrapper(service string)
PurgeStaleService()
UpdateServiceEntryEnpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string)
UpdateServiceEntryEndpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string)
GetServiceByEndpoints(requestVersions, endpoints map[string]bool, versionKey string, protocol common.Protocol) map[string][]string
GetAllServiceEntry() []*v1alpha3.ServiceEntry
GetAllServiceEntryWrapper() []*ServiceEntryWrapper
@@ -58,7 +58,7 @@ type store struct {
deferedDelete map[string]struct{}
}
func (s *store) UpdateServiceEntryEnpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string) {
func (s *store) UpdateServiceEntryEndpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string) {
s.mux.Lock()
defer s.mux.Unlock()
if se, exist := s.sew[service]; exist {

View File

@@ -66,7 +66,7 @@ type watcher struct {
isStop bool
addrProvider *address.NacosAddressProvider
updateCacheWhenEmpty bool
nacosClietConfig *constant.ClientConfig
nacosClientConfig *constant.ClientConfig
authOption provider.AuthOption
}
@@ -94,7 +94,7 @@ func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, er
log.Infof("new nacos2 watcher with config Name:%s", w.Name)
w.nacosClietConfig = constant.NewClientConfig(
w.nacosClientConfig = constant.NewClientConfig(
constant.WithTimeoutMs(DefaultNacosTimeout),
constant.WithLogLevel(DefaultNacosLogLevel),
constant.WithLogDir(DefaultNacosLogDir),
@@ -129,7 +129,7 @@ func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, er
success := make(chan struct{})
go func() {
namingClient, err := clients.NewNamingClient(vo.NacosClientParam{
ClientConfig: w.nacosClietConfig,
ClientConfig: w.nacosClientConfig,
ServerConfigs: sc,
})
if err == nil {
@@ -267,7 +267,7 @@ func (w *watcher) updateNacosClient() {
defer w.mutex.Unlock()
w.Domain = addr
namingClient, err := clients.NewNamingClient(vo.NacosClientParam{
ClientConfig: w.nacosClietConfig,
ClientConfig: w.nacosClientConfig,
ServerConfigs: []constant.ServerConfig{
*constant.NewServerConfig(addr, uint64(w.Port)),
},

View File

@@ -241,7 +241,7 @@ func (w *watcher) fetchAllServices(firstFetch ...bool) error {
case SpringCloudService:
serviceConfig.UrlIndex = path.Join(serviceInfo.rootPath, serviceInfo.service)
default:
return errors.New("unkown type")
return errors.New("unknown type")
}
serviceConfigs = append(serviceConfigs, serviceConfig)
}
@@ -275,7 +275,7 @@ func (w *watcher) ListenService() {
}
log.Errorf("[Zookeeper][ListenService] Get children of path zkRootPath with watcher failed, err:%v, index:%s", err, listIndex.UrlIndex)
// May be the provider does not ready yet, sleep failTimes * ConnDelay senconds to wait
// May be the provider does not ready yet, sleep failTimes * ConnDelay seconds to wait
after := time.After(timeSecondDuration(failTimes * ConnDelay))
select {
case <-after:
@@ -384,7 +384,7 @@ func (w *watcher) GetInterfaceConfig(event Event) (string, *InterfaceConfig, err
}
}
func (w *watcher) GetSpringCloudConfig(intefaceName string, content []byte) (string, *InterfaceConfig, error) {
func (w *watcher) GetSpringCloudConfig(interfaceName string, content []byte) (string, *InterfaceConfig, error) {
var instance SpringCloudInstance
err := json.Unmarshal(content, &instance)
if err != nil {
@@ -392,7 +392,7 @@ func (w *watcher) GetSpringCloudConfig(intefaceName string, content []byte) (str
return "", nil, err
}
var config InterfaceConfig
host := intefaceName
host := interfaceName
config.Host = host
config.Protocol = common.HTTP.String()
if len(instance.Payload.Metadata) > 0 && instance.Payload.Metadata["protocol"] != "" {

View File

@@ -32,7 +32,7 @@ It can be divided into below steps:
4. kube-load-image: load dev higress-controller image it into kind cluster.
5. install-dev: install higress-controller with dev image, and latest higress-gateway, istiod with helm.
6. run-e2e-test:
1. Setup conformance suite, like define what conformance tests we want to run, in `e2e_test.go` / `higressTests Slice`. Each case we choose to open is difined in `test/ingress/conformance/tests`.
1. Setup conformance suite, like define what conformance tests we want to run, in `e2e_test.go` / `higressTests Slice`. Each case we choose to open is defined in `test/ingress/conformance/tests`.
2. Prepare resources and install them into cluster, like backend services/deployments.
3. Load conformance tests we choose to open in `e2e_test.go` / `higressTests Slice`, and run them one by one, fail if it is not expected.
@@ -67,7 +67,7 @@ The test environment reusability is primarily achieved through the following tar
## Gateway APIs Conformance Tests
Gateway API Conformance tests are based on the suite provided by `kubernetes-sig/gateway-api`, we can reuse that,
and descide what conformance tests we need to open. Conformance tests of Gateway API.
and decide what conformance tests we need to open. Conformance tests of Gateway API.
This API covers a broad set of features and use cases and has been implemented widely.
This combination of both a large feature set and variety of implementations requires

View File

@@ -32,15 +32,15 @@ Higress 提供了运行 Ingress API 一致性测试和 wasmplugin 测试的 make
4. kube-load-image: 将 dev higress-controller 镜像加载到 kind 集群中。
5. install-dev: 使用 helm 安装带有 dev 镜像的 higress-controller并安装最新的 higress-gateway、istiod。
6. run-e2e-test:
1. 设置一致性测试套件,例如在 `e2e_test.go` / `higressTests Slice` 中定义我们想要运行的一致性测试。我们选择打开的每个测试都在 `test/ingress/conformance/tests` 中定义
1. 所有测试都在 `test/e2e/conformance/tests` 中定义,并在初始化阶段被注册到`ConformanceTests`中。`ConormanceTests` 是一个全局变量,用于存储所有的一致性测试用例
2. 准备资源并将它们安装到集群中,例如后端服务/部署。
3. `e2e_test.go` / `higressTests Slice` 中加载我们选择打开的一致性测试,并逐个运行它们,如果不符合预期,则失败。
3. 加载选择打开的一致性测试,并逐个运行它们,如果不符合预期,则失败。
### 如何编写测试用例
要添加新的测试用例,首先需要在 `test/ingress/conformance/tests` 中添加 `xxx.go``xxx.yaml``xxx.yaml` 是您需要在集群中应用的 Ingress 资源,`xxx.go` 定义了 HigressConformanceTest。
然后,您应该将您定义的 HigressConformanceTest 添加到 `e2e_test.go` / `higressTests Slice`
然后,您应该将您定义的测试用例注册到`ConormanceTests`方法是在xxx.go中使用`init()` 函数调用`Register(YOUR_PLUGIN_SHORT_NAME)`
通过查看 `test/ingress/conformance/tests/httproute-simple-same-namespace.go``test/ingress/conformance/tests/httproute-simple-same-namespace.yaml` 中的代码,您可以快速了解并尝试编写一个测试用例。
@@ -57,10 +57,10 @@ Higress 提供了运行 Ingress API 一致性测试和 wasmplugin 测试的 make
- **make higress-conformance-test-clean:** 可用于清理 higress-controller、higress-gateway 等 deployment 的测试环境。
2. **make higress-wasmplugin-test:** 用于运行整个 WasmPlugin 测试流程,包括搭建测试环境、编译 WasmPlugin 插件、运行测试用例、清理测试环境。
- **make higress-wasmplugin-test-prepare:** 可用于搭建 higress-controller、higress-gateway 等 deployment 的环境,并编译 WasmPlugin 插件。
- **make higress-wasmplugin-test-prepare:** 可用于搭建 higress-controller、higress-gateway 等 deployment 的环境,并编译 WasmPlugin 插件。通过设置变量`PLUGIN_NAME`仅编译指定插件。
- **make run-higress-e2e-test-wasmplugin:** 可用于运行测试用例。
- **make run-higress-e2e-test-wasmplugin-setup:** 可用于安装测试用例所需的基础资源,例如 nacos、dubbo 等。
- **make run-higress-e2e-test-wasmplugin-run:** 可用于运行测试用例。
- **make run-higress-e2e-test-wasmplugin-run:** 可用于运行测试用例。通过设置变量`TEST_SHORTNAME`仅运行指定测试。
- **make run-higress-e2e-test-wasmplugin-clean:** 可用于清理测试用例在 setup 阶段所安装的基础资源。
- **make higress-wasmplugin-test-clean:** 可用于清理 higress-controller、higress-gateway 等 deployment 的测试环境。