diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index ad2622051910..547a155bd0a5 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -329,7 +329,7 @@ func (t *Translator) processHTTPRouteRule(httpRoute *HTTPRouteContext, ruleIdx i if err != nil { return nil, err } - sessionPersistence.Cookie.TTL = &ttl + sessionPersistence.Cookie.TTL = &metav1.Duration{Duration: ttl} } } } diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 2c9f4ad06ce0..cfd372e27ab0 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -11,7 +11,6 @@ import ( "net/http" "net/netip" "reflect" - "time" "golang.org/x/exp/slices" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -588,7 +587,7 @@ type SessionPersistence struct { // CookieBasedSessionPersistence defines the configuration for cookie-based session persistence. // +k8s:deepcopy-gen=true type CookieBasedSessionPersistence struct { - TTL *time.Duration `json:"ttl,omitempty" yaml:"ttl,omitempty"` + TTL *metav1.Duration `json:"ttl,omitempty" yaml:"ttl,omitempty"` } // HeaderBasedSessionPersistence defines the configuration for header-based session persistence. diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 75e9d3b31804..3b6ab98159fa 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -14,7 +14,6 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/gateway-api/apis/v1alpha2" - timex "time" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -537,7 +536,7 @@ func (in *CookieBasedSessionPersistence) DeepCopyInto(out *CookieBasedSessionPer *out = *in if in.TTL != nil { in, out := &in.TTL, &out.TTL - *out = new(timex.Duration) + *out = new(v1.Duration) **out = **in } } diff --git a/internal/xds/translator/session_persistence.go b/internal/xds/translator/session_persistence.go index 7484b13c05ac..3e7372629f83 100644 --- a/internal/xds/translator/session_persistence.go +++ b/internal/xds/translator/session_persistence.go @@ -66,7 +66,7 @@ func (s *sessionPersistence) patchHCM(mgr *hcmv3.HttpConnectionManager, irListen Cookie: &httpv3.Cookie{ Name: sp.SessionName, Path: routePathToCookiePath(route.PathMatch), - Ttl: durationpb.New(*sp.Cookie.TTL), + Ttl: durationpb.New(sp.Cookie.TTL.Duration), }, } case sp.Header != nil: diff --git a/internal/xds/translator/session_persistence_test.go b/internal/xds/translator/session_persistence_test.go deleted file mode 100644 index e2ceb4bd6d59..000000000000 --- a/internal/xds/translator/session_persistence_test.go +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright Envoy Gateway Authors -// SPDX-License-Identifier: Apache-2.0 -// The full text of the Apache license is available in the LICENSE file at -// the root of the repo. - -package translator - -import ( - "testing" - "time" - - hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - 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" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/durationpb" - "k8s.io/utils/ptr" - - "github.com/envoyproxy/gateway/internal/ir" -) - -func Test_sessionPersistence_patchHCM(t *testing.T) { - t.Parallel() - type args struct { - mgr *hcmv3.HttpConnectionManager - irListener *ir.HTTPListener - } - tests := []struct { - name string - s *sessionPersistence - args args - wantErr bool - wantMgr *hcmv3.HttpConnectionManager - }{ - { - name: "nil hcm", - s: &sessionPersistence{}, - args: args{ - // nil mgr - irListener: &ir.HTTPListener{}, - }, - wantErr: true, - }, - { - name: "nil irListener", - s: &sessionPersistence{}, - args: args{ - mgr: &hcmv3.HttpConnectionManager{}, - }, - wantErr: true, - }, - { - // patchHCM should return early without any edit if the filter already exists - // because patchHCM could be called multiple times for the same filter. - name: "mgr already has the filter", - s: &sessionPersistence{}, - args: args{ - irListener: &ir.HTTPListener{}, - mgr: &hcmv3.HttpConnectionManager{ - HttpFilters: []*hcmv3.HttpFilter{ - { - Name: sessionPersistenceFilter, - }, - }, - }, - }, - wantMgr: &hcmv3.HttpConnectionManager{ - HttpFilters: []*hcmv3.HttpFilter{ - { - Name: sessionPersistenceFilter, - }, - }, - }, - wantErr: false, - }, - { - name: "no session persistence", - s: &sessionPersistence{}, - args: args{ - mgr: &hcmv3.HttpConnectionManager{}, - irListener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Name: "route1", - // no session persistence config - }, - }, - }, - }, - wantMgr: &hcmv3.HttpConnectionManager{}, - }, - { - name: "no session persistence", - s: &sessionPersistence{}, - args: args{ - mgr: &hcmv3.HttpConnectionManager{}, - irListener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Name: "route1", - // no session persistence config - }, - }, - }, - }, - wantMgr: &hcmv3.HttpConnectionManager{}, - }, - { - name: "header-based session persistence", - s: &sessionPersistence{}, - args: args{ - mgr: &hcmv3.HttpConnectionManager{}, - irListener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Name: "route1", - SessionPersistence: &ir.SessionPersistence{ - SessionName: "session1", - Header: &ir.HeaderBasedSessionPersistence{}, - }, - }, - }, - }, - }, - wantMgr: &hcmv3.HttpConnectionManager{ - HttpFilters: []*hcmv3.HttpFilter{ - { - Name: sessionPersistenceFilter, - ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: mustAnyPB(&headerv3.HeaderBasedSessionState{ - Name: "session1", - }), - }, - }, - }, - }, - }, - { - name: "cookie-based session persistence (one route)", - s: &sessionPersistence{}, - args: args{ - mgr: &hcmv3.HttpConnectionManager{}, - irListener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Name: "route1", - SessionPersistence: &ir.SessionPersistence{ - SessionName: "session1", - Cookie: &ir.CookieBasedSessionPersistence{ - TTL: ptr.To(time.Duration(10)), - }, - }, - }, - }, - }, - }, - wantMgr: &hcmv3.HttpConnectionManager{ - HttpFilters: []*hcmv3.HttpFilter{ - { - Name: sessionPersistenceFilter, - ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: mustAnyPB(&cookiev3.CookieBasedSessionState{ - Cookie: &httpv3.Cookie{ - Name: "session1", - Ttl: durationpb.New(time.Duration(10)), - Path: "/", - }, - }), - }, - }, - }, - }, - }, - { - name: "cookie-based session persistence (multiple routes)", - s: &sessionPersistence{}, - args: args{ - mgr: &hcmv3.HttpConnectionManager{}, - irListener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Name: "route1", - PathMatch: &ir.StringMatch{ - Prefix: ptr.To("/v1"), - }, - SessionPersistence: &ir.SessionPersistence{ - SessionName: "session1", - Cookie: &ir.CookieBasedSessionPersistence{ - TTL: ptr.To(time.Duration(10)), - }, - }, - }, - { - Name: "route2", - PathMatch: &ir.StringMatch{ - SafeRegex: ptr.To("/v2/.*/abc"), - }, - SessionPersistence: &ir.SessionPersistence{ - SessionName: "session1", - Cookie: &ir.CookieBasedSessionPersistence{ - TTL: ptr.To(time.Duration(10)), - }, - }, - }, - }, - }, - }, - wantMgr: &hcmv3.HttpConnectionManager{ - HttpFilters: []*hcmv3.HttpFilter{ - { - Name: sessionPersistenceFilter, - ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: mustAnyPB(&cookiev3.CookieBasedSessionState{ - Cookie: &httpv3.Cookie{ - Name: "session1", - Ttl: durationpb.New(time.Duration(10)), - Path: "/v1", - }, - }), - }, - }, - { - Name: sessionPersistenceFilter, - ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: mustAnyPB(&cookiev3.CookieBasedSessionState{ - Cookie: &httpv3.Cookie{ - Name: "session1", - Ttl: durationpb.New(time.Duration(10)), - Path: "/v2", - }, - }), - }, - }, - }, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - s := &sessionPersistence{} - mgr := tt.args.mgr - err := s.patchHCM(mgr, tt.args.irListener) - if (err != nil) != tt.wantErr { - t.Errorf("sessionPersistence.patchHCM() error = %v, wantErr %v", err, tt.wantErr) - } - if err != nil { - // No need to continue - return - } - - if diff := cmp.Diff(tt.wantMgr, mgr, cmpopts.IgnoreUnexported( - hcmv3.HttpConnectionManager{}, - hcmv3.HttpFilter{}, - headerv3.HeaderBasedSessionState{}, - hcmv3.HttpFilter_TypedConfig{}, - anypb.Any{})); diff != "" { - t.Errorf("sessionPersistence.patchHCM() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func mustAnyPB(m proto.Message) *anypb.Any { - a, err := anypb.New(m) - if err != nil { - panic(err) - } - return a -} diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-session-persistence.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-session-persistence.yaml new file mode 100644 index 000000000000..b7c300863a61 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-session-persistence.yaml @@ -0,0 +1,65 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + routes: + - name: "header-based-session-persistence-route" + hostname: "*" + pathMatch: + safeRegex: "/v1/.*" + sessionPersistence: + sessionName: "session1" + header: {} + destination: + name: "regex-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "cookie-based-session-persistence-route-regex" + hostname: "*" + pathMatch: + safeRegex: "/v1/.*/hoge" + sessionPersistence: + sessionName: "session1" + cookie: + ttl: "1h" + destination: + name: "regex-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "cookie-based-session-persistence-route-prefix" + hostname: "*" + pathMatch: + prefix: "/v2/" + sessionPersistence: + sessionName: "session1" + cookie: + ttl: "1h" + destination: + name: "regex-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "cookie-based-session-persistence-route-exact" + hostname: "*" + pathMatch: + exact: "/v3/user" + sessionPersistence: + sessionName: "session1" + cookie: + ttl: "1h" + destination: + name: "regex-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 \ No newline at end of file diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.clusters.yaml new file mode 100644 index 000000000000..0f75e67e2789 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.clusters.yaml @@ -0,0 +1,17 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: regex-route-dest + lbPolicy: LEAST_REQUEST + name: regex-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.endpoints.yaml new file mode 100644 index 000000000000..b36ee4500599 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.endpoints.yaml @@ -0,0 +1,12 @@ +- clusterName: regex-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: regex-route-dest/backend/0 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 new file mode 100644 index 000000000000..25eb7f650366 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.listeners.yaml @@ -0,0 +1,60 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.stateful_session + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.stateful_session.header.v3.HeaderBasedSessionState + name: session1 + - name: envoy.filters.http.stateful_session + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState + cookie: + name: session1 + path: /v1 + ttl: 3600s + - name: envoy.filters.http.stateful_session + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState + cookie: + name: session1 + path: /v2/ + ttl: 3600s + - name: envoy.filters.http.stateful_session + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.stateful_session.cookie.v3.CookieBasedSessionState + cookie: + name: session1 + path: /v3/user + ttl: 3600s + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http + useRemoteAddress: true + name: first-listener + drainType: MODIFY_ONLY + name: first-listener + perConnectionBufferLimitBytes: 32768 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 new file mode 100644 index 000000000000..fa1955a0a6ea --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-session-persistence.routes.yaml @@ -0,0 +1,37 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + safeRegex: + regex: /v1/.* + name: header-based-session-persistence-route + route: + cluster: regex-route-dest + upgradeConfigs: + - upgradeType: websocket + - match: + safeRegex: + regex: /v1/.*/hoge + name: cookie-based-session-persistence-route-regex + route: + cluster: regex-route-dest + upgradeConfigs: + - upgradeType: websocket + - match: + pathSeparatedPrefix: /v2 + name: cookie-based-session-persistence-route-prefix + route: + cluster: regex-route-dest + upgradeConfigs: + - upgradeType: websocket + - match: + path: /v3/user + name: cookie-based-session-persistence-route-exact + route: + cluster: regex-route-dest + upgradeConfigs: + - upgradeType: websocket