Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 111 additions & 51 deletions internal/xds/translator/session_persistence.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import (
cookiev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/stateful_session/cookie/v3"
headerv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/stateful_session/header/v3"
httpv3 "github.com/envoyproxy/go-control-plane/envoy/type/http/v3"
"google.golang.org/protobuf/proto"
protobuf "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/ir"
"github.com/envoyproxy/gateway/internal/utils/proto"
"github.com/envoyproxy/gateway/internal/xds/types"
)

Expand All @@ -46,72 +47,112 @@ func (s *sessionPersistence) patchHCM(mgr *hcmv3.HttpConnectionManager, irListen
if mgr == nil {
return errors.New("hcm is nil")
}

if irListener == nil {
return errors.New("ir listener is nil")
}
if hcmContainsFilter(mgr, egv1a1.EnvoyFilterSessionPersistence.String()) {
return nil
}

var (
routeWithSessionPersistence *ir.HTTPRoute
filter *hcmv3.HttpFilter
err error
)

for _, route := range irListener.Routes {
sp := route.SessionPersistence
if sp == nil {
continue
if route.SessionPersistence != nil {
routeWithSessionPersistence = route
break
}
}

if hcmContainsFilter(mgr, perRouteFilterName(egv1a1.EnvoyFilterSessionPersistence, route.Name)) {
continue
}
if routeWithSessionPersistence == nil {
return nil
}

var sessionCfg proto.Message
var configName string
switch {
case sp.Cookie != nil:
configName = cookieConfigName
cookieCfg := &cookiev3.CookieBasedSessionState{
Cookie: &httpv3.Cookie{
Name: sp.Cookie.Name,
Path: routePathToCookiePath(route.PathMatch),
},
}

if sp.Cookie.TTL != nil {
cookieCfg.Cookie.Ttl = durationpb.New(sp.Cookie.TTL.Duration)
}

sessionCfg = cookieCfg
case sp.Header != nil:
configName = headerConfigName
sessionCfg = &headerv3.HeaderBasedSessionState{
Name: sp.Header.Name,
}
}
// We use the first route that contains the session persistence config to build the filter.
// The HCM-level filter config doesn't matter since it is overridden at the route level.
if filter, err = buildHCMStatefulSessionFilter(routeWithSessionPersistence); err != nil {
return err
}
mgr.HttpFilters = append(mgr.HttpFilters, filter)
return nil
}

sessionCfgAny, err := anypb.New(sessionCfg)
if err != nil {
return fmt.Errorf("failed to marshal %s config: %w", egv1a1.EnvoyFilterSessionPersistence.String(), err)
}
// buildHCMStatefulSessionFilter returns a stateful_session HTTP filter from the provided IR SessionPersistence.
func buildHCMStatefulSessionFilter(route *ir.HTTPRoute) (*hcmv3.HttpFilter, error) {
statefulSessionProto, err := buildStatefulSessionFilterConfig(route)
if err != nil {
return nil, fmt.Errorf("failed to build stateful session filter config: %w",
err)
}
statefulSessionAny, err := proto.ToAnyWithValidation(statefulSessionProto)
if err != nil {
return nil, err
}

cfg := &statefulsessionv3.StatefulSession{
SessionState: &corev3.TypedExtensionConfig{
Name: configName,
TypedConfig: sessionCfgAny,
return &hcmv3.HttpFilter{
Name: egv1a1.EnvoyFilterSessionPersistence.String(),
ConfigType: &hcmv3.HttpFilter_TypedConfig{
TypedConfig: statefulSessionAny,
},
Disabled: true,
}, nil
}

func buildStatefulSessionFilterConfig(route *ir.HTTPRoute) (*statefulsessionv3.StatefulSession, error) {
var (
sessionCfg protobuf.Message
configName string
sp = route.SessionPersistence
)

switch {
case sp.Cookie != nil:
configName = cookieConfigName
cookieCfg := &cookiev3.CookieBasedSessionState{
Cookie: &httpv3.Cookie{
Name: sp.Cookie.Name,
Path: routePathToCookiePath(route.PathMatch),
},
}

cfgAny, err := anypb.New(cfg)
if err != nil {
return fmt.Errorf("failed to marshal %s config: %w", egv1a1.EnvoyFilterSessionPersistence.String(), err)
if sp.Cookie.TTL != nil {
cookieCfg.Cookie.Ttl = durationpb.New(sp.Cookie.TTL.Duration)
}

mgr.HttpFilters = append(mgr.HttpFilters, &hcmv3.HttpFilter{
Name: perRouteFilterName(egv1a1.EnvoyFilterSessionPersistence, route.Name),
Disabled: true,
ConfigType: &hcmv3.HttpFilter_TypedConfig{
TypedConfig: cfgAny,
},
})
sessionCfg = cookieCfg
case sp.Header != nil:
configName = headerConfigName
sessionCfg = &headerv3.HeaderBasedSessionState{
Name: sp.Header.Name,
}
}

return nil
sessionCfgAny, err := anypb.New(sessionCfg)
if err != nil {
return nil, fmt.Errorf("failed to marshal %s config: %w", egv1a1.EnvoyFilterSessionPersistence.String(), err)
}

return &statefulsessionv3.StatefulSession{
SessionState: &corev3.TypedExtensionConfig{
Name: configName,
TypedConfig: sessionCfgAny,
},
}, nil
}

func buildStatefulSessionFilterPerRouteConfig(route *ir.HTTPRoute) (*statefulsessionv3.StatefulSessionPerRoute, error) {
statefulSession, err := buildStatefulSessionFilterConfig(route)
if err != nil {
return nil, err
}
return &statefulsessionv3.StatefulSessionPerRoute{
Override: &statefulsessionv3.StatefulSessionPerRoute_StatefulSession{
StatefulSession: statefulSession,
},
}, nil
}

func routePathToCookiePath(path *ir.StringMatch) string {
Expand Down Expand Up @@ -160,10 +201,29 @@ func (s *sessionPersistence) patchRoute(route *routev3.Route, irRoute *ir.HTTPRo
return nil
}

if err := enableFilterOnRoute(route, perRouteFilterName(egv1a1.EnvoyFilterSessionPersistence, route.Name)); err != nil {
perFilterCfg := route.GetTypedPerFilterConfig()
if _, ok := perFilterCfg[egv1a1.EnvoyFilterSessionPersistence.String()]; ok {
// This should not happen since this is the only place where the filter
// config is added in a route.
return fmt.Errorf("route already contains filter config: %s, %+v",
egv1a1.EnvoyFilterSessionPersistence.String(), route)
}

// Overwrite the HCM level filter config with the per route filter config.
statefulSessionProto, err := buildStatefulSessionFilterPerRouteConfig(irRoute)
if err != nil {
return fmt.Errorf("failed to build stateful session filter config: %w", err)
}
statefulSessionAny, err := proto.ToAnyWithValidation(statefulSessionProto)
if err != nil {
return err
}

if perFilterCfg == nil {
route.TypedPerFilterConfig = make(map[string]*anypb.Any)
}
route.TypedPerFilterConfig[egv1a1.EnvoyFilterSessionPersistence.String()] = statefulSessionAny

return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,14 @@
maxConcurrentStreams: 100
httpFilters:
- disabled: true
name: envoy.filters.http.stateful_session/header-based-session-persistence-route
name: envoy.filters.http.stateful_session
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSession
sessionState:
name: envoy.http.stateful_session.header
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.header.v3.HeaderBasedSessionState
name: session-header
- disabled: true
name: envoy.filters.http.stateful_session/cookie-based-session-persistence-route-regex
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSession
sessionState:
name: envoy.http.stateful_session.cookie
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState
cookie:
name: session-header
path: /v1
ttl: 3600s
- disabled: true
name: envoy.filters.http.stateful_session/cookie-based-session-persistence-route-prefix
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSession
sessionState:
name: envoy.http.stateful_session.cookie
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState
cookie:
name: session-header
path: /v2/
- disabled: true
name: envoy.filters.http.stateful_session/cookie-based-session-persistence-route-exact
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSession
sessionState:
name: envoy.http.stateful_session.cookie
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState
cookie:
name: session-cookie
path: /v3/user
ttl: 3600s
- name: envoy.filters.http.router
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
envoy.filters.http.stateful_session/header-based-session-persistence-route:
'@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
config: {}
envoy.filters.http.stateful_session:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSessionPerRoute
statefulSession:
sessionState:
name: envoy.http.stateful_session.header
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.header.v3.HeaderBasedSessionState
name: session-header
- match:
safeRegex:
regex: /v1/.*/hoge
Expand All @@ -26,9 +31,17 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
envoy.filters.http.stateful_session/cookie-based-session-persistence-route-regex:
'@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
config: {}
envoy.filters.http.stateful_session:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSessionPerRoute
statefulSession:
sessionState:
name: envoy.http.stateful_session.cookie
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState
cookie:
name: session-header
path: /v1
ttl: 3600s
- match:
pathSeparatedPrefix: /v2
name: cookie-based-session-persistence-route-prefix
Expand All @@ -37,9 +50,16 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
envoy.filters.http.stateful_session/cookie-based-session-persistence-route-prefix:
'@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
config: {}
envoy.filters.http.stateful_session:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSessionPerRoute
statefulSession:
sessionState:
name: envoy.http.stateful_session.cookie
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState
cookie:
name: session-header
path: /v2/
- match:
path: /v3/user
name: cookie-based-session-persistence-route-exact
Expand All @@ -48,6 +68,14 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
envoy.filters.http.stateful_session/cookie-based-session-persistence-route-exact:
'@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
config: {}
envoy.filters.http.stateful_session:
'@type': type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSessionPerRoute
statefulSession:
sessionState:
name: envoy.http.stateful_session.cookie
typedConfig:
'@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState
cookie:
name: session-cookie
path: /v3/user
ttl: 3600s
1 change: 1 addition & 0 deletions release-notes/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ bug fixes: |
Skipped ExtProc, Wasm, and ExtAuth when they are configured FailOpen and the configuration is invalid, e.g. missing backendRefs or invalid port.
Fixed issue that failed to update policy status when there are more than 16 ancestors.
Fixed race condition in watchable.Map Snapshot subscription.
Fixed issue where HTTPRoutes with sessionPersistence caused the Envoy listeners to drain.

# Enhancements that improve performance.
performance improvements: |
Expand Down