xds/internal/xdsclient: Add support for String Matcher Header Matcher in RDS (#6313)

This commit is contained in:
Zach Reyes 2023-05-25 18:05:14 -04:00 committed by GitHub
parent 157db1907e
commit 4d3f221d1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 171 additions and 2 deletions

View File

@ -241,3 +241,34 @@ func (hcm *HeaderContainsMatcher) Match(md metadata.MD) bool {
func (hcm *HeaderContainsMatcher) String() string {
return fmt.Sprintf("headerContains:%v%v", hcm.key, hcm.contains)
}
// HeaderStringMatcher matches on whether the header value matches against the
// StringMatcher specified.
type HeaderStringMatcher struct {
key string
stringMatcher StringMatcher
invert bool
}
// NewHeaderStringMatcher returns a new HeaderStringMatcher.
func NewHeaderStringMatcher(key string, sm StringMatcher, invert bool) *HeaderStringMatcher {
return &HeaderStringMatcher{
key: key,
stringMatcher: sm,
invert: invert,
}
}
// Match returns whether the passed in HTTP Headers match according to the
// specified StringMatcher.
func (hsm *HeaderStringMatcher) Match(md metadata.MD) bool {
v, ok := mdValuesFromOutgoingCtx(md, hsm.key)
if !ok {
return false
}
return hsm.stringMatcher.Match(v) != hsm.invert
}
func (hsm *HeaderStringMatcher) String() string {
return fmt.Sprintf("headerString:%v:%v", hsm.key, hsm.stringMatcher)
}

View File

@ -467,3 +467,83 @@ func TestHeaderSuffixMatcherMatch(t *testing.T) {
})
}
}
func TestHeaderStringMatch(t *testing.T) {
tests := []struct {
name string
key string
sm StringMatcher
invert bool
md metadata.MD
want bool
}{
{
name: "should-match",
key: "th",
sm: StringMatcher{
exactMatch: newStringP("tv"),
},
invert: false,
md: metadata.Pairs("th", "tv"),
want: true,
},
{
name: "not match",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: false,
md: metadata.Pairs("th", "not-match"),
want: false,
},
{
name: "invert string match",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: true,
md: metadata.Pairs("th", "not-match"),
want: true,
},
{
name: "header missing",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: false,
md: metadata.Pairs("not-specified-key", "not-match"),
want: false,
},
{
name: "header missing invert true",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: true,
md: metadata.Pairs("not-specified-key", "not-match"),
want: false,
},
{
name: "header empty string invert",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: true,
md: metadata.Pairs("th", ""),
want: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
hsm := NewHeaderStringMatcher(test.key, test.sm, test.invert)
if got := hsm.Match(test.md); got != test.want {
t.Errorf("match() = %v, want %v", got, test.want)
}
})
}
}

View File

@ -59,6 +59,8 @@ func RouteToMatcher(r *Route) (*CompositeMatcher, error) {
matcherT = matcher.NewHeaderRangeMatcher(h.Name, h.RangeMatch.Start, h.RangeMatch.End, invert)
case h.PresentMatch != nil:
matcherT = matcher.NewHeaderPresentMatcher(h.Name, *h.PresentMatch, invert)
case h.StringMatch != nil:
matcherT = matcher.NewHeaderStringMatcher(h.Name, *h.StringMatch, invert)
default:
return nil, fmt.Errorf("illegal route: missing header_match_specifier")
}

View File

@ -171,6 +171,7 @@ type HeaderMatcher struct {
SuffixMatch *string
RangeMatch *Int64Range
PresentMatch *bool
StringMatch *matcher.StringMatcher
}
// Int64Range is a range for header range match.

View File

@ -24,13 +24,15 @@ import (
"strings"
"time"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/xds/matcher"
"google.golang.org/grpc/xds/internal/clusterspecifier"
"google.golang.org/protobuf/types/known/anypb"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
)
func unmarshalRouteConfigResource(r *anypb.Any) (string, RouteConfigUpdate, error) {
@ -273,6 +275,12 @@ func routesProtoToSlice(routes []*v3routepb.Route, csps map[string]clusterspecif
header.PrefixMatch = &ht.PrefixMatch
case *v3routepb.HeaderMatcher_SuffixMatch:
header.SuffixMatch = &ht.SuffixMatch
case *v3routepb.HeaderMatcher_StringMatch:
sm, err := matcher.StringMatcherFromProto(ht.StringMatch)
if err != nil {
return nil, nil, fmt.Errorf("route %+v has an invalid string matcher: %v", err, ht.StringMatch)
}
header.StringMatch = &sm
default:
return nil, nil, fmt.Errorf("route %+v has an unrecognized header matcher: %+v", r, ht)
}

View File

@ -33,6 +33,7 @@ import (
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/pretty"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/xds/matcher"
"google.golang.org/grpc/xds/internal/clusterspecifier"
"google.golang.org/grpc/xds/internal/httpfilter"
"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
@ -923,6 +924,7 @@ func (s) TestUnmarshalRouteConfig(t *testing.T) {
}
func (s) TestRoutesProtoToSlice(t *testing.T) {
sm, _ := matcher.StringMatcherFromProto(&v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "tv"}})
var (
goodRouteWithFilterConfigs = func(cfgs map[string]*anypb.Any) []*v3routepb.Route {
// Sets per-filter config in cluster "B" and in the route.
@ -1085,6 +1087,51 @@ func (s) TestRoutesProtoToSlice(t *testing.T) {
}},
wantErr: false,
},
{
name: "good with string matcher",
routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "/a/"}},
Headers: []*v3routepb.HeaderMatcher{
{
Name: "th",
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "tv"}}},
},
},
RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
DefaultValue: &v3typepb.FractionalPercent{
Numerator: 1,
Denominator: v3typepb.FractionalPercent_HUNDRED,
},
},
},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
WeightedClusters: &v3routepb.WeightedCluster{
Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
},
}}}},
},
},
wantRoutes: []*Route{{
Regex: func() *regexp.Regexp { return regexp.MustCompile("/a/") }(),
Headers: []*HeaderMatcher{
{
Name: "th",
InvertMatch: newBoolP(false),
StringMatch: &sm,
},
},
Fraction: newUInt32P(10000),
WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
ActionType: RouteActionRoute,
}},
wantErr: false,
},
{
name: "query is ignored",
routes: []*v3routepb.Route{