mirror of https://github.com/grpc/grpc-go.git
xds/internal/xdsclient: Add support for String Matcher Header Matcher in RDS (#6313)
This commit is contained in:
parent
157db1907e
commit
4d3f221d1d
|
@ -241,3 +241,34 @@ func (hcm *HeaderContainsMatcher) Match(md metadata.MD) bool {
|
||||||
func (hcm *HeaderContainsMatcher) String() string {
|
func (hcm *HeaderContainsMatcher) String() string {
|
||||||
return fmt.Sprintf("headerContains:%v%v", hcm.key, hcm.contains)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,8 @@ func RouteToMatcher(r *Route) (*CompositeMatcher, error) {
|
||||||
matcherT = matcher.NewHeaderRangeMatcher(h.Name, h.RangeMatch.Start, h.RangeMatch.End, invert)
|
matcherT = matcher.NewHeaderRangeMatcher(h.Name, h.RangeMatch.Start, h.RangeMatch.End, invert)
|
||||||
case h.PresentMatch != nil:
|
case h.PresentMatch != nil:
|
||||||
matcherT = matcher.NewHeaderPresentMatcher(h.Name, *h.PresentMatch, invert)
|
matcherT = matcher.NewHeaderPresentMatcher(h.Name, *h.PresentMatch, invert)
|
||||||
|
case h.StringMatch != nil:
|
||||||
|
matcherT = matcher.NewHeaderStringMatcher(h.Name, *h.StringMatch, invert)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("illegal route: missing header_match_specifier")
|
return nil, fmt.Errorf("illegal route: missing header_match_specifier")
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,6 +171,7 @@ type HeaderMatcher struct {
|
||||||
SuffixMatch *string
|
SuffixMatch *string
|
||||||
RangeMatch *Int64Range
|
RangeMatch *Int64Range
|
||||||
PresentMatch *bool
|
PresentMatch *bool
|
||||||
|
StringMatch *matcher.StringMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64Range is a range for header range match.
|
// Int64Range is a range for header range match.
|
||||||
|
|
|
@ -24,13 +24,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
"github.com/golang/protobuf/proto"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/internal/envconfig"
|
"google.golang.org/grpc/internal/envconfig"
|
||||||
|
"google.golang.org/grpc/internal/xds/matcher"
|
||||||
"google.golang.org/grpc/xds/internal/clusterspecifier"
|
"google.golang.org/grpc/xds/internal/clusterspecifier"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"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) {
|
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
|
header.PrefixMatch = &ht.PrefixMatch
|
||||||
case *v3routepb.HeaderMatcher_SuffixMatch:
|
case *v3routepb.HeaderMatcher_SuffixMatch:
|
||||||
header.SuffixMatch = &ht.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:
|
default:
|
||||||
return nil, nil, fmt.Errorf("route %+v has an unrecognized header matcher: %+v", r, ht)
|
return nil, nil, fmt.Errorf("route %+v has an unrecognized header matcher: %+v", r, ht)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import (
|
||||||
"google.golang.org/grpc/internal/envconfig"
|
"google.golang.org/grpc/internal/envconfig"
|
||||||
"google.golang.org/grpc/internal/pretty"
|
"google.golang.org/grpc/internal/pretty"
|
||||||
"google.golang.org/grpc/internal/testutils"
|
"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/clusterspecifier"
|
||||||
"google.golang.org/grpc/xds/internal/httpfilter"
|
"google.golang.org/grpc/xds/internal/httpfilter"
|
||||||
"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
|
"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) {
|
func (s) TestRoutesProtoToSlice(t *testing.T) {
|
||||||
|
sm, _ := matcher.StringMatcherFromProto(&v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "tv"}})
|
||||||
var (
|
var (
|
||||||
goodRouteWithFilterConfigs = func(cfgs map[string]*anypb.Any) []*v3routepb.Route {
|
goodRouteWithFilterConfigs = func(cfgs map[string]*anypb.Any) []*v3routepb.Route {
|
||||||
// Sets per-filter config in cluster "B" and in the route.
|
// Sets per-filter config in cluster "B" and in the route.
|
||||||
|
@ -1085,6 +1087,51 @@ func (s) TestRoutesProtoToSlice(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
wantErr: false,
|
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",
|
name: "query is ignored",
|
||||||
routes: []*v3routepb.Route{
|
routes: []*v3routepb.Route{
|
||||||
|
|
Loading…
Reference in New Issue