diff --git a/internal/xds/translator/session_persistence.go b/internal/xds/translator/session_persistence.go index c8e64e4ec8..f68a18a80d 100644 --- a/internal/xds/translator/session_persistence.go +++ b/internal/xds/translator/session_persistence.go @@ -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" ) @@ -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 { @@ -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 } diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.listeners.yaml index 26dedfa4ee..2b97daa228 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.listeners.yaml @@ -15,7 +15,7 @@ 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: @@ -23,41 +23,6 @@ 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 diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.routes.yaml index c5450601be..52f05f15b8 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.routes.yaml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 7e23d86a53..8e0ac1c890 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -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: |