Skip to content

Commit

Permalink
feat: support session persistence in HTTPRouteRule
Browse files Browse the repository at this point in the history
Signed-off-by: sanposhiho <44139130+sanposhiho@users.noreply.github.com>
  • Loading branch information
sanposhiho committed Jul 14, 2024
1 parent 7b09c21 commit d33c8f3
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 6 deletions.
4 changes: 2 additions & 2 deletions examples/extension-server/cmd/extension-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import (
"os/signal"
"syscall"

pb "github.com/envoyproxy/gateway/proto/extension"
"github.com/exampleorg/envoygateway-extension/internal/extensionserver"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"

"github.com/exampleorg/envoygateway-extension/internal/extensionserver"
pb "github.com/envoyproxy/gateway/proto/extension"
)

func main() {
Expand Down
4 changes: 2 additions & 2 deletions examples/extension-server/internal/extensionserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import (
"fmt"
"log/slog"

pb "github.com/envoyproxy/gateway/proto/extension"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
bav3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/basic_auth/v3"
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/exampleorg/envoygateway-extension/api/v1alpha1"
"google.golang.org/protobuf/types/known/anypb"

"github.com/exampleorg/envoygateway-extension/api/v1alpha1"
pb "github.com/envoyproxy/gateway/proto/extension"
)

type Server struct {
Expand Down
23 changes: 22 additions & 1 deletion internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,33 @@ func (t *Translator) processHTTPRouteRule(httpRoute *HTTPRouteContext, ruleIdx i
ruleRoutes = append(ruleRoutes, irRoute)
}

var sessionPersistence *ir.SessionPersistence
if rule.SessionPersistence != nil {
sessionPersistence = &ir.SessionPersistence{
SessionName: *rule.SessionPersistence.SessionName,
}
if rule.SessionPersistence.Type != nil && *rule.SessionPersistence.Type == gwapiv1.HeaderBasedSessionPersistence {
sessionPersistence.Header = &ir.HeaderBasedSessionPersistence{}
} else {
// Cookie-based session persistence is default.
sessionPersistence.Cookie = &ir.CookieBasedSessionPersistence{}
if rule.SessionPersistence.AbsoluteTimeout != nil {
ttl, err := time.ParseDuration(string(*rule.SessionPersistence.AbsoluteTimeout))
if err != nil {
return nil, err
}
sessionPersistence.Cookie.TTL = &ttl
}
}
}

// A rule is matched if any one of its matches
// is satisfied (i.e. a logical "OR"), so generate
// a unique Xds IR HTTPRoute per match.
for matchIdx, match := range rule.Matches {
irRoute := &ir.HTTPRoute{
Name: irRouteName(httpRoute, ruleIdx, matchIdx),
Name: irRouteName(httpRoute, ruleIdx, matchIdx),
SessionPersistence: sessionPersistence,
}
processTimeout(irRoute, rule)

Expand Down
24 changes: 24 additions & 0 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/netip"
"reflect"
"time"

"golang.org/x/exp/slices"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
Expand Down Expand Up @@ -566,8 +567,31 @@ type HTTPRoute struct {
UseClientProtocol *bool `json:"useClientProtocol,omitempty" yaml:"useClientProtocol,omitempty"`
// Metadata is used to enrich envoy route metadata with user and provider-specific information
Metadata *ResourceMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"`
// SessionPersistence holds the configuration for session persistence.
SessionPersistence *SessionPersistence `json:"sessionPersistence,omitempty" yaml:"sessionPersistence,omitempty"`
}

// SessionPersistence defines the desired state of SessionPersistence.
type SessionPersistence struct {
// SessionName defines the name of the persistent session token.
SessionName string `json:"sessionName,omitempty"`

// Cookie defines the configuration for cookie-based session persistence.
// Either Cookie or Header must be non-empty.
Cookie *CookieBasedSessionPersistence `json:"cookie,omitempty"`
// Header defines the configuration for header-based session persistence.
// Either Cookie or Header must be non-empty.
Header *HeaderBasedSessionPersistence `json:"header,omitempty"`
}

// CookieBasedSessionPersistence defines the configuration for cookie-based session persistence.
type CookieBasedSessionPersistence struct {
TTL *time.Duration `json:"ttl,omitempty"`
}

// HeaderBasedSessionPersistence defines the configuration for header-based session persistence.
type HeaderBasedSessionPersistence struct{}

// TrafficFeatures holds the information associated with the Backend Traffic Policy.
// +k8s:deepcopy-gen=true
type TrafficFeatures struct {
Expand Down
5 changes: 5 additions & 0 deletions internal/ir/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion internal/xds/translator/httpfilters.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter {
order = 5
case isFilterType(filter, jwtAuthn):
order = 6
case isFilterType(filter, sessionPersistenceFilter):
order = 7
case isFilterType(filter, extProcFilter):
order = 7 + mustGetFilterIndex(filter.Name)
order = 8 + mustGetFilterIndex(filter.Name)
case isFilterType(filter, wasmFilter):
order = 100 + mustGetFilterIndex(filter.Name)
case isFilterType(filter, string(egv1a1.EnvoyFilterRBAC)):
Expand Down
140 changes: 140 additions & 0 deletions internal/xds/translator/session_persistence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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 (
"errors"
"strings"

routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
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"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/envoyproxy/gateway/internal/ir"
"github.com/envoyproxy/gateway/internal/xds/types"
)

const (
sessionPersistenceFilter = "envoy.filters.http.stateful_session"
)

type sessionPersistence struct{}

func init() {
registerHTTPFilter(&sessionPersistence{})
}

var _ httpFilter = &sessionPersistence{}

// patchHCM patches the HttpConnectionManager with the filter.
// Note: this method may be called multiple times for the same filter, please
// make sure to avoid duplicate additions of the same filter.
func (s *sessionPersistence) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error {
if mgr == nil {
return errors.New("hcm is nil")
}

if irListener == nil {
return errors.New("ir listener is nil")
}

// Return early if filter already exists.
for _, f := range mgr.HttpFilters {
if f.Name == sessionPersistenceFilter {
return nil
}
}

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

var cfg proto.Message
switch {
case sp.Cookie != nil:
cfg = &cookiev3.CookieBasedSessionState{
Cookie: &httpv3.Cookie{
Name: sp.SessionName,
Path: routePathToCookiePath(route.PathMatch),
Ttl: durationpb.New(*sp.Cookie.TTL),
},
}
case sp.Header != nil:
cfg = &headerv3.HeaderBasedSessionState{
Name: sp.SessionName,
}
}

cfgAny, err := anypb.New(cfg)
if err != nil {
return err
}

mgr.HttpFilters = append(mgr.HttpFilters, &hcmv3.HttpFilter{
Name: sessionPersistenceFilter,
ConfigType: &hcmv3.HttpFilter_TypedConfig{
TypedConfig: cfgAny,
},
})
}
return nil
}

func routePathToCookiePath(path *ir.StringMatch) string {
if path == nil {
return "/"
}
switch {
case path.Exact != nil:
return *path.Exact
case path.Prefix != nil:
return *path.Prefix
case path.SafeRegex != nil:
return getLongestNonRegexPrefix(*path.SafeRegex)
}

// Shouldn't reach here because the path should be either of the above three kinds.
return "/"
}

// getLongestNonRegexPrefix takes a regex path and returns the longest non-regex prefix.
// > 3. For an xRoute using a path that is a regex, the Path should be set to the longest non-regex prefix
// (.e.g. if the path is /p1/p2/*/p3 and the request path was /p1/p2/foo/p3, then the cookie path would be /p1/p2).
// https://gateway-api.sigs.k8s.io/geps/gep-1619/#path
func getLongestNonRegexPrefix(path string) string {
parts := strings.Split(path, "/")
var longestNonRegexPrefix []string
for _, part := range parts {
if part == "*" || strings.Contains(part, "*") {
break
}
longestNonRegexPrefix = append(longestNonRegexPrefix, part)
}

return strings.Join(longestNonRegexPrefix, "/")
}

// patchRoute patches the provide Route with a filter's Route level configuration.
func (s *sessionPersistence) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error {
return nil
}

// patchResources adds all the other needed resources referenced by this
// filter to the resource version table.
// for example:
// - a jwt filter needs to add the cluster for the jwks.
// - an oidc filter needs to add the cluster for token endpoint and the secret
// for the oauth2 client secret and the hmac secret.
func (s *sessionPersistence) patchResources(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error {
return nil
}
Loading

0 comments on commit d33c8f3

Please sign in to comment.