mirror of
https://github.com/alibaba/higress.git
synced 2026-03-09 19:20:51 +08:00
feat: add MCP SSE stateful session load balancer support (#2818)
This commit is contained in:
@@ -137,6 +137,8 @@ endif
|
||||
# for now docker is limited to Linux compiles - why ?
|
||||
include docker/docker.mk
|
||||
|
||||
docker-build-amd64: docker.higress-amd64 ## Build and push amdd64 docker images to registry defined by $HUB and $TAG
|
||||
|
||||
docker-build: docker.higress ## Build and push docker images to registry defined by $HUB and $TAG
|
||||
|
||||
docker-buildx-push: clean-env docker.higress-buildx
|
||||
@@ -144,7 +146,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.8/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.9/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -192,7 +194,7 @@ install: pre-install
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 48da465cfd0dc5c9ac851bd2b9743780dc82dd8a
|
||||
ISTIO_LATEST_IMAGE_TAG ?= latest
|
||||
|
||||
install-dev: pre-install
|
||||
|
||||
@@ -6,11 +6,11 @@ ARG BASE_VERSION=latest
|
||||
|
||||
ARG HUB
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
# The following section is used as base image if BASE_DISTRIBUTION=debug
|
||||
# This base image is provided by istio, see: https://github.com/istio/istio/blob/master/docker/Dockerfile.base
|
||||
FROM ${HUB}/base:${BASE_VERSION}
|
||||
|
||||
ARG TARGETARCH
|
||||
FROM ${HUB}/base:${BASE_VERSION}-${TARGETARCH}
|
||||
|
||||
COPY ${TARGETARCH:-amd64}/higress /usr/local/bin/higress
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ docker.higress: $(OUT_LINUX)/higress
|
||||
docker.higress: docker/Dockerfile.higress
|
||||
$(HIGRESS_DOCKER_RULE)
|
||||
|
||||
docker.higress-amd64: BUILD_ARGS=--build-arg BASE_VERSION=${HIGRESS_BASE_VERSION} --build-arg HUB=${HUB}
|
||||
docker.higress-amd64: $(AMD64_OUT_LINUX)/higress
|
||||
docker.higress-amd64: docker/Dockerfile.higress
|
||||
$(HIGRESS_DOCKER_AMD64_RULE)
|
||||
|
||||
docker.higress-buildx: BUILD_ARGS=--build-arg BASE_VERSION=${HIGRESS_BASE_VERSION} --build-arg HUB=${HUB}
|
||||
docker.higress-buildx: $(AMD64_OUT_LINUX)/higress
|
||||
docker.higress-buildx: $(ARM64_OUT_LINUX)/higress
|
||||
@@ -40,3 +45,4 @@ IMG_URL ?= $(HUB)/$(IMG):$(TAG)
|
||||
|
||||
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
|
||||
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
|
||||
HIGRESS_DOCKER_AMD64_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=amd64 ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) --build-arg TARGETARCH=amd64 -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
|
||||
|
||||
Submodule envoy/envoy updated: e2707255f1...7f18940fbc
Submodule istio/proxy updated: d411a4f019...ced6d8167a
@@ -628,6 +628,7 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
|
||||
mappings := map[string]*common.Rule{}
|
||||
|
||||
initHttp2RpcGlobalConfig := true
|
||||
initMcpSseGlobalFilter := true
|
||||
for _, routes := range convertOptions.HTTPRoutes {
|
||||
for _, route := range routes {
|
||||
if strings.HasSuffix(route.HTTPRoute.Name, "app-root") {
|
||||
@@ -647,6 +648,19 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
|
||||
}
|
||||
}
|
||||
|
||||
loadBalance := route.WrapperConfig.AnnotationsConfig.LoadBalance
|
||||
if loadBalance != nil && loadBalance.McpSseStateful {
|
||||
IngressLog.Infof("Found MCP SSE stateful session for route %s", route.HTTPRoute.Name)
|
||||
envoyFilter, err := m.constructMcpSseStatefulSessionEnvoyFilter(route, m.namespace, initMcpSseGlobalFilter)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Construct MCP SSE stateful session EnvoyFilter error %v", err)
|
||||
} else {
|
||||
IngressLog.Infof("Append MCP SSE stateful session EnvoyFilter for route %s", route.HTTPRoute.Name)
|
||||
envoyFilters = append(envoyFilters, *envoyFilter)
|
||||
initMcpSseGlobalFilter = false
|
||||
}
|
||||
}
|
||||
|
||||
auth := route.WrapperConfig.AnnotationsConfig.Auth
|
||||
if auth == nil {
|
||||
continue
|
||||
@@ -1511,7 +1525,7 @@ func (m *IngressConfig) constructHttp2RpcEnvoyFilter(http2rpcConfig *annotations
|
||||
return &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "http2rpc", http2rpcConfig.Name, "route", httpRoute.Name),
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "http2rpc", http2rpcConfig.Name, "route", common.ConvertToDNSLabelValid(httpRoute.Name)),
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
@@ -1940,6 +1954,99 @@ func (m *IngressConfig) Delete(config.GroupVersionKind, string, string, *string)
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.WrapperHTTPRoute, namespace string, initGlobalFilter bool) (*config.Config, error) {
|
||||
httpRoute := route.HTTPRoute
|
||||
|
||||
var configPatches []*networking.EnvoyFilter_EnvoyConfigObjectPatch
|
||||
|
||||
// Add global HTTP filter if this is the first route using MCP SSE stateful session
|
||||
if initGlobalFilter {
|
||||
configPatches = append(configPatches, &networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.router",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
|
||||
Value: buildPatchStruct(`{
|
||||
"name": "envoy.filters.http.mcp_sse_stateful_session",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.mcp_sse_stateful_session.v3alpha.McpSseStatefulSession"
|
||||
}
|
||||
}`),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add route-specific configuration
|
||||
configPatches = append(configPatches, &networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_ROUTE,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_RouteConfiguration{
|
||||
RouteConfiguration: &networking.EnvoyFilter_RouteConfigurationMatch{
|
||||
Vhost: &networking.EnvoyFilter_RouteConfigurationMatch_VirtualHostMatch{
|
||||
Route: &networking.EnvoyFilter_RouteConfigurationMatch_RouteMatch{
|
||||
Name: httpRoute.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: buildPatchStruct(`{
|
||||
"typed_per_filter_config": {
|
||||
"envoy.filters.http.mcp_sse_stateful_session": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.mcp_sse_stateful_session.v3alpha.McpSseStatefulSessionPerRoute",
|
||||
"value": {
|
||||
"mcp_sse_stateful_session": {
|
||||
"session_state": {
|
||||
"name": "envoy.http.mcp_sse_stateful_session.envelope",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.http.mcp_sse_stateful_session.envelope.v3alpha.EnvelopeSessionState",
|
||||
"value": {
|
||||
"param_name": "sessionId",
|
||||
"chunk_end_patterns": ["\r\n\r\n", "\n\n", "\r\r"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
})
|
||||
|
||||
return &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "mcp-lb-route", common.ConvertToDNSLabelValid(httpRoute.Name)),
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: configPatches,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(gvk config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
var configsUpdated map[istiomodel.ConfigKey]struct{}
|
||||
if updatedConfigName != nil {
|
||||
|
||||
@@ -66,9 +66,10 @@ type consistentHashByCookie struct {
|
||||
}
|
||||
|
||||
type LoadBalanceConfig struct {
|
||||
simple networking.LoadBalancerSettings_SimpleLB
|
||||
other *consistentHashByOther
|
||||
cookie *consistentHashByCookie
|
||||
simple networking.LoadBalancerSettings_SimpleLB
|
||||
other *consistentHashByOther
|
||||
cookie *consistentHashByCookie
|
||||
McpSseStateful bool
|
||||
}
|
||||
|
||||
type loadBalance struct{}
|
||||
@@ -129,7 +130,11 @@ func (l loadBalance) Parse(annotations Annotations, config *Ingress, _ *GlobalCo
|
||||
} else {
|
||||
if lb, err := annotations.ParseStringASAP(loadBalanceAnnotation); err == nil {
|
||||
lb = strings.ToUpper(lb)
|
||||
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
|
||||
if lb == "MCP-SSE" {
|
||||
loadBalanceConfig.McpSseStateful = true
|
||||
} else {
|
||||
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ func GetHost(annotations map[string]string) string {
|
||||
|
||||
// Istio requires that the name of the gateway must conform to the DNS label.
|
||||
// For details, you can view: https://github.com/istio/istio/blob/2d5c40ad5e9cceebe64106005aa38381097da2ba/pkg/config/validation/validation.go#L478
|
||||
func convertToDNSLabelValid(input string) string {
|
||||
func ConvertToDNSLabelValid(input string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(input))
|
||||
hash := hasher.Sum(nil)
|
||||
@@ -156,7 +156,7 @@ func convertToDNSLabelValid(input string) string {
|
||||
|
||||
// CleanHost follow the format of mse-ops for host.
|
||||
func CleanHost(host string) string {
|
||||
return convertToDNSLabelValid(host)
|
||||
return ConvertToDNSLabelValid(host)
|
||||
}
|
||||
|
||||
func CreateConvertedName(items ...string) string {
|
||||
|
||||
67
test/e2e/conformance/tests/ingress-loadbalance-mcp-sse.go
Normal file
67
test/e2e/conformance/tests/ingress-loadbalance-mcp-sse.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/envoy"
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(IngressLoadBalanceMcpSse)
|
||||
}
|
||||
|
||||
var IngressLoadBalanceMcpSse = suite.ConformanceTest{
|
||||
ShortName: "IngressLoadBalanceMcpSse",
|
||||
Description: "The Envoy config should contain MCP SSE stateful session filter when load-balance annotation is set to mcp-sse",
|
||||
Manifests: []string{"tests/ingress-loadbalance-mcp-sse.yaml"},
|
||||
Features: []suite.SupportedFeature{suite.EnvoyConfigConformanceFeature},
|
||||
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
envoyAssertion []envoy.Assertion
|
||||
}{
|
||||
{
|
||||
name: "MCP SSE stateful session global filter should be added",
|
||||
envoyAssertion: []envoy.Assertion{
|
||||
{
|
||||
Path: "configs.#.dynamic_listeners.#.active_state.listener.filter_chains.#.filters.#.typed_config.http_filters",
|
||||
CheckType: envoy.CheckTypeMatch,
|
||||
TargetNamespace: "higress-system",
|
||||
ExpectEnvoyConfig: map[string]interface{}{
|
||||
"name": "envoy.filters.http.mcp_sse_stateful_session",
|
||||
"typed_config": map[string]interface{}{
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.mcp_sse_stateful_session.v3alpha.McpSseStatefulSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: add per router filter check
|
||||
}
|
||||
|
||||
t.Run("Ingress LoadBalance MCP SSE", func(t *testing.T) {
|
||||
for _, testcase := range testCases {
|
||||
t.Logf("Test Case %s", testcase.name)
|
||||
for _, assertion := range testcase.envoyAssertion {
|
||||
envoy.AssertEnvoyConfig(t, suite.TimeoutConfig, assertion)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
34
test/e2e/conformance/tests/ingress-loadbalance-mcp-sse.yaml
Normal file
34
test/e2e/conformance/tests/ingress-loadbalance-mcp-sse.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: higress-conformance-infra-ingress-loadbalance-mcp-sse-test
|
||||
namespace: higress-conformance-infra
|
||||
annotations:
|
||||
higress.io/load-balance: "mcp-sse"
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "mcp-sse.example.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/api"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v3
|
||||
port:
|
||||
number: 8080
|
||||
Reference in New Issue
Block a user