connect: allow L7 routers to match on http methods (#6164)

Fixes #6158
This commit is contained in:
R.B. Boyer 2019-07-23 20:56:39 -05:00 committed by GitHub
parent 67f3da61af
commit bd4a2d7be2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 8 deletions

View File

@ -3119,6 +3119,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
"match": { "match": {
"http": { "http": {
"path_prefix": "/foo", "path_prefix": "/foo",
"methods": [ "GET", "DELETE" ],
"query_param": [ "query_param": [
{ {
"name": "hack1", "name": "hack1",
@ -3202,6 +3203,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
match { match {
http { http {
path_prefix = "/foo" path_prefix = "/foo"
methods = [ "GET", "DELETE" ]
query_param = [ query_param = [
{ {
name = "hack1" name = "hack1"
@ -3284,6 +3286,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
Match: &structs.ServiceRouteMatch{ Match: &structs.ServiceRouteMatch{
HTTP: &structs.ServiceRouteHTTPMatch{ HTTP: &structs.ServiceRouteHTTPMatch{
PathPrefix: "/foo", PathPrefix: "/foo",
Methods: []string{"GET", "DELETE"},
QueryParam: []structs.ServiceRouteHTTPMatchQueryParam{ QueryParam: []structs.ServiceRouteHTTPMatchQueryParam{
{ {
Name: "hack1", Name: "hack1",

View File

@ -58,6 +58,21 @@ func (e *ServiceRouterConfigEntry) Normalize() error {
e.Kind = ServiceRouter e.Kind = ServiceRouter
for _, route := range e.Routes {
if route.Match == nil || route.Match.HTTP == nil {
continue
}
httpMatch := route.Match.HTTP
if len(httpMatch.Methods) == 0 {
continue
}
for j := 0; j < len(httpMatch.Methods); j++ {
httpMatch.Methods[j] = strings.ToUpper(httpMatch.Methods[j])
}
}
return nil return nil
} }
@ -139,6 +154,16 @@ func (e *ServiceRouterConfigEntry) Validate() error {
return fmt.Errorf("Route[%d] QueryParam[%d] should only contain one of Present, Exact, or Regex", i, j) return fmt.Errorf("Route[%d] QueryParam[%d] should only contain one of Present, Exact, or Regex", i, j)
} }
} }
if len(route.Match.HTTP.Methods) > 0 {
found := make(map[string]struct{})
for _, m := range route.Match.HTTP.Methods {
if _, ok := found[m]; ok {
return fmt.Errorf("Route[%d] Methods contains %q more than once", i, m)
}
found[m] = struct{}{}
}
}
} }
if route.Destination != nil { if route.Destination != nil {
@ -218,9 +243,7 @@ type ServiceRouteHTTPMatch struct {
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"` Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"` QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
Methods []string `json:",omitempty"`
// TODO(rb): reenable Methods
// Methods []string `json:",omitempty"`
} }
func (m *ServiceRouteHTTPMatch) IsEmpty() bool { func (m *ServiceRouteHTTPMatch) IsEmpty() bool {
@ -228,8 +251,8 @@ func (m *ServiceRouteHTTPMatch) IsEmpty() bool {
m.PathPrefix == "" && m.PathPrefix == "" &&
m.PathRegex == "" && m.PathRegex == "" &&
len(m.Header) == 0 && len(m.Header) == 0 &&
len(m.QueryParam) == 0 len(m.QueryParam) == 0 &&
// && len(m.Methods) == 0 len(m.Methods) == 0
} }
type ServiceRouteHTTPMatchHeader struct { type ServiceRouteHTTPMatchHeader struct {

View File

@ -1099,6 +1099,28 @@ func TestServiceRouterConfigEntry(t *testing.T) {
}), }),
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix", validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
}, },
////////////////
{
name: "route with method matches",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
Methods: []string{
"get", "POST", "dElEtE",
},
}))),
check: func(t *testing.T, entry *ServiceRouterConfigEntry) {
m := entry.Routes[0].Match.HTTP.Methods
require.Equal(t, []string{"GET", "POST", "DELETE"}, m)
},
},
{
name: "route with method matches repeated",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
Methods: []string{
"GET", "DELETE", "get",
},
}))),
validateErr: "Methods contains \"GET\" more than once",
},
} }
for _, tc := range cases { for _, tc := range cases {

View File

@ -168,6 +168,7 @@ func TestDecodeConfigEntry(t *testing.T) {
match { match {
http { http {
path_prefix = "/foo" path_prefix = "/foo"
methods = [ "GET", "DELETE" ]
query_param = [ query_param = [
{ {
name = "hack1" name = "hack1"
@ -246,6 +247,7 @@ func TestDecodeConfigEntry(t *testing.T) {
Match { Match {
HTTP { HTTP {
PathPrefix = "/foo" PathPrefix = "/foo"
Methods = [ "GET", "DELETE" ]
QueryParam = [ QueryParam = [
{ {
Name = "hack1" Name = "hack1"
@ -324,6 +326,7 @@ func TestDecodeConfigEntry(t *testing.T) {
Match: &ServiceRouteMatch{ Match: &ServiceRouteMatch{
HTTP: &ServiceRouteHTTPMatch{ HTTP: &ServiceRouteHTTPMatch{
PathPrefix: "/foo", PathPrefix: "/foo",
Methods: []string{"GET", "DELETE"},
QueryParam: []ServiceRouteHTTPMatchQueryParam{ QueryParam: []ServiceRouteHTTPMatchQueryParam{
{ {
Name: "hack1", Name: "hack1",

View File

@ -3,6 +3,7 @@ package xds
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/gogo/protobuf/proto" "github.com/gogo/protobuf/proto"
@ -252,6 +253,18 @@ func makeRouteMatchForDiscoveryRoute(discoveryRoute *structs.DiscoveryRoute, pro
} }
} }
if len(match.HTTP.Methods) > 0 {
methodHeaderRegex := strings.Join(match.HTTP.Methods, "|")
eh := &envoyroute.HeaderMatcher{
Name: ":method",
HeaderMatchSpecifier: &envoyroute.HeaderMatcher_RegexMatch{
RegexMatch: methodHeaderRegex,
},
}
em.Headers = append(em.Headers, eh)
}
if len(match.HTTP.QueryParam) > 0 { if len(match.HTTP.QueryParam) > 0 {
em.QueryParameters = make([]*envoyroute.QueryParameterMatcher, 0, len(match.HTTP.QueryParam)) em.QueryParameters = make([]*envoyroute.QueryParameterMatcher, 0, len(match.HTTP.QueryParam))
for _, qm := range match.HTTP.QueryParam { for _, qm := range match.HTTP.QueryParam {

View File

@ -190,6 +190,24 @@ func TestRoutesFromSnapshot(t *testing.T) {
}), }),
Destination: toService("hdr-regex"), Destination: toService("hdr-regex"),
}, },
{
Match: httpMatch(&structs.ServiceRouteHTTPMatch{
Methods: []string{"GET", "PUT"},
}),
Destination: toService("just-methods"),
},
{
Match: httpMatch(&structs.ServiceRouteHTTPMatch{
Header: []structs.ServiceRouteHTTPMatchHeader{
{
Name: "x-debug",
Exact: "exact",
},
},
Methods: []string{"GET", "PUT"},
}),
Destination: toService("hdr-exact-with-method"),
},
{ {
Match: httpMatchParam(structs.ServiceRouteHTTPMatchQueryParam{ Match: httpMatchParam(structs.ServiceRouteHTTPMatchQueryParam{
Name: "secretparam1", Name: "secretparam1",

View File

@ -120,6 +120,38 @@
"cluster": "hdr-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" "cluster": "hdr-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
} }
}, },
{
"match": {
"prefix": "/",
"headers": [
{
"name": ":method",
"regexMatch": "GET|PUT"
}
]
},
"route": {
"cluster": "just-methods.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"exactMatch": "exact"
},
{
"name": ":method",
"regexMatch": "GET|PUT"
}
]
},
"route": {
"cluster": "hdr-exact-with-method.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{ {
"match": { "match": {
"prefix": "/", "prefix": "/",

View File

@ -33,9 +33,7 @@ type ServiceRouteHTTPMatch struct {
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"` Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"` QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
Methods []string `json:",omitempty"`
// TODO(rb): reenable Methods
// Methods []string `json:",omitempty"`
} }
type ServiceRouteHTTPMatchHeader struct { type ServiceRouteHTTPMatchHeader struct {

View File

@ -262,6 +262,7 @@ func TestParseConfigEntry(t *testing.T) {
match { match {
http { http {
path_prefix = "/foo" path_prefix = "/foo"
methods = [ "GET", "DELETE" ]
query_param = [ query_param = [
{ {
name = "hack1" name = "hack1"
@ -340,6 +341,7 @@ func TestParseConfigEntry(t *testing.T) {
Match { Match {
HTTP { HTTP {
PathPrefix = "/foo" PathPrefix = "/foo"
Methods = [ "GET", "DELETE" ]
QueryParam = [ QueryParam = [
{ {
Name = "hack1" Name = "hack1"
@ -418,6 +420,7 @@ func TestParseConfigEntry(t *testing.T) {
Match: &api.ServiceRouteMatch{ Match: &api.ServiceRouteMatch{
HTTP: &api.ServiceRouteHTTPMatch{ HTTP: &api.ServiceRouteHTTPMatch{
PathPrefix: "/foo", PathPrefix: "/foo",
Methods: []string{"GET", "DELETE"},
QueryParam: []api.ServiceRouteHTTPMatchQueryParam{ QueryParam: []api.ServiceRouteHTTPMatchQueryParam{
{ {
Name: "hack1", Name: "hack1",

View File

@ -193,6 +193,9 @@ routes = [
At most only one of `Exact`, `Regex`, or `Present` may be configured. At most only one of `Exact`, `Regex`, or `Present` may be configured.
- `Methods` `(array<string>)` - A list of HTTP methods for which this match
applies. If unspecified all http methods are matched.
- `Destination` `(ServiceRouteDestination: <optional>)` - Controls how to - `Destination` `(ServiceRouteDestination: <optional>)` - Controls how to
proxy the actual matching request to a service. proxy the actual matching request to a service.