connect: validate and test more of the L7 config entries (#6156)
This commit is contained in:
parent
05889ccc47
commit
fc90beb925
|
@ -3082,7 +3082,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "debug2",
|
"name": "debug2",
|
||||||
"present": false,
|
"present": true,
|
||||||
"invert": true
|
"invert": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3165,7 +3165,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "debug2"
|
name = "debug2"
|
||||||
present = false
|
present = true
|
||||||
invert = true
|
invert = true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3247,7 +3247,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "debug2",
|
Name: "debug2",
|
||||||
Present: false,
|
Present: true,
|
||||||
Invert: true,
|
Invert: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,8 +3,10 @@ package structs
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
|
@ -54,12 +56,8 @@ func (e *ServiceRouterConfigEntry) Normalize() error {
|
||||||
return fmt.Errorf("config entry is nil")
|
return fmt.Errorf("config entry is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(rb): trim spaces
|
|
||||||
|
|
||||||
e.Kind = ServiceRouter
|
e.Kind = ServiceRouter
|
||||||
|
|
||||||
// TODO(rb): anything to normalize?
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,78 +66,69 @@ func (e *ServiceRouterConfigEntry) Validate() error {
|
||||||
return fmt.Errorf("Name is required")
|
return fmt.Errorf("Name is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(rb): enforce corresponding service has protocol=http
|
|
||||||
|
|
||||||
// TODO(rb): actually you can only define the HTTP section if protocol=http{,2}
|
|
||||||
|
|
||||||
// TODO(rb): validate the entire compiled chain? how?
|
|
||||||
|
|
||||||
// TODO(rb): validate more
|
|
||||||
|
|
||||||
// Technically you can have no explicit routes at all where just the
|
// Technically you can have no explicit routes at all where just the
|
||||||
// catch-all is configured for you, but at that point maybe you should just
|
// catch-all is configured for you, but at that point maybe you should just
|
||||||
// delete it so it will default?
|
// delete it so it will default?
|
||||||
|
|
||||||
for i, route := range e.Routes {
|
for i, route := range e.Routes {
|
||||||
if route.Match == nil || route.Match.HTTP == nil {
|
eligibleForPrefixRewrite := false
|
||||||
continue
|
if route.Match != nil && route.Match.HTTP != nil {
|
||||||
}
|
pathParts := 0
|
||||||
|
if route.Match.HTTP.PathExact != "" {
|
||||||
|
eligibleForPrefixRewrite = true
|
||||||
|
pathParts++
|
||||||
|
if !strings.HasPrefix(route.Match.HTTP.PathExact, "/") {
|
||||||
|
return fmt.Errorf("Route[%d] PathExact doesn't start with '/': %q", i, route.Match.HTTP.PathExact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if route.Match.HTTP.PathPrefix != "" {
|
||||||
|
eligibleForPrefixRewrite = true
|
||||||
|
pathParts++
|
||||||
|
if !strings.HasPrefix(route.Match.HTTP.PathPrefix, "/") {
|
||||||
|
return fmt.Errorf("Route[%d] PathPrefix doesn't start with '/': %q", i, route.Match.HTTP.PathPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if route.Match.HTTP.PathRegex != "" {
|
||||||
|
pathParts++
|
||||||
|
}
|
||||||
|
if pathParts > 1 {
|
||||||
|
return fmt.Errorf("Route[%d] should only contain at most one of PathExact, PathPrefix, or PathRegex", i)
|
||||||
|
}
|
||||||
|
|
||||||
pathParts := 0
|
for j, hdr := range route.Match.HTTP.Header {
|
||||||
if route.Match.HTTP.PathExact != "" {
|
if hdr.Name == "" {
|
||||||
pathParts++
|
return fmt.Errorf("Route[%d] Header[%d] missing required Name field", i, j)
|
||||||
}
|
}
|
||||||
if route.Match.HTTP.PathPrefix != "" {
|
hdrParts := 0
|
||||||
pathParts++
|
if hdr.Present {
|
||||||
}
|
hdrParts++
|
||||||
if route.Match.HTTP.PathRegex != "" {
|
}
|
||||||
pathParts++
|
if hdr.Exact != "" {
|
||||||
}
|
hdrParts++
|
||||||
if pathParts > 1 {
|
}
|
||||||
return fmt.Errorf("Route[%d] should only contain at most one of PathExact, PathPrefix, or PathRegex", i)
|
if hdr.Regex != "" {
|
||||||
}
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdr.Prefix != "" {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdr.Suffix != "" {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdrParts != 1 {
|
||||||
|
return fmt.Errorf("Route[%d] Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex", i, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(rb): do some validation of PathExact and PathPrefix
|
for j, qm := range route.Match.HTTP.QueryParam {
|
||||||
|
if qm.Name == "" {
|
||||||
for j, hdr := range route.Match.HTTP.Header {
|
return fmt.Errorf("Route[%d] QueryParam[%d] missing required Name field", i, j)
|
||||||
if hdr.Name == "" {
|
}
|
||||||
return fmt.Errorf("Route[%d] Header[%d] missing required Name field", i, j)
|
|
||||||
}
|
}
|
||||||
hdrParts := 0
|
|
||||||
if hdr.Present {
|
|
||||||
hdrParts++
|
|
||||||
}
|
|
||||||
if hdr.Exact != "" {
|
|
||||||
hdrParts++
|
|
||||||
}
|
|
||||||
if hdr.Regex != "" {
|
|
||||||
hdrParts++
|
|
||||||
}
|
|
||||||
if hdr.Prefix != "" {
|
|
||||||
hdrParts++
|
|
||||||
}
|
|
||||||
if hdr.Suffix != "" {
|
|
||||||
hdrParts++
|
|
||||||
}
|
|
||||||
// "absent" is the bare invert=true
|
|
||||||
if (hdrParts == 0 && !hdr.Invert) || (hdrParts > 1) {
|
|
||||||
return fmt.Errorf("Route[%d] Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex (or just Invert)", i, j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for j, qm := range route.Match.HTTP.QueryParam {
|
|
||||||
if qm.Name == "" {
|
|
||||||
return fmt.Errorf("Route[%d] QueryParam[%d] missing required Name field", i, j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ineligibleForPrefixRewrite := false
|
|
||||||
if route.Match.HTTP.PathRegex != "" {
|
|
||||||
ineligibleForPrefixRewrite = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if route.Destination != nil {
|
if route.Destination != nil {
|
||||||
if route.Destination.PrefixRewrite != "" && ineligibleForPrefixRewrite {
|
if route.Destination.PrefixRewrite != "" && !eligibleForPrefixRewrite {
|
||||||
return fmt.Errorf("Route[%d] cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix", i)
|
return fmt.Errorf("Route[%d] cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,6 +165,10 @@ func (e *ServiceRouterConfigEntry) ListRelatedServices() []string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(found) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
out := make([]string, 0, len(found))
|
out := make([]string, 0, len(found))
|
||||||
for svc, _ := range found {
|
for svc, _ := range found {
|
||||||
out = append(out, svc)
|
out = append(out, svc)
|
||||||
|
@ -336,7 +329,6 @@ func (e *ServiceSplitterConfigEntry) Normalize() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return fmt.Errorf("config entry is nil")
|
return fmt.Errorf("config entry is nil")
|
||||||
}
|
}
|
||||||
// TODO(rb): trim spaces
|
|
||||||
|
|
||||||
e.Kind = ServiceSplitter
|
e.Kind = ServiceSplitter
|
||||||
|
|
||||||
|
@ -398,10 +390,6 @@ func (e *ServiceSplitterConfigEntry) Validate() error {
|
||||||
return fmt.Errorf("the sum of all split weights must be 100, not %f", float32(sumScaled)/100)
|
return fmt.Errorf("the sum of all split weights must be 100, not %f", float32(sumScaled)/100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(rb): enforce corresponding service has protocol=http
|
|
||||||
|
|
||||||
// TODO(rb): validate the entire compiled chain? how?
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,6 +426,10 @@ func (e *ServiceSplitterConfigEntry) ListRelatedServices() []string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(found) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
out := make([]string, 0, len(found))
|
out := make([]string, 0, len(found))
|
||||||
for svc, _ := range found {
|
for svc, _ := range found {
|
||||||
out = append(out, svc)
|
out = append(out, svc)
|
||||||
|
@ -568,12 +560,9 @@ func (e *ServiceResolverConfigEntry) Normalize() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return fmt.Errorf("config entry is nil")
|
return fmt.Errorf("config entry is nil")
|
||||||
}
|
}
|
||||||
// TODO(rb): trim spaces
|
|
||||||
|
|
||||||
e.Kind = ServiceResolver
|
e.Kind = ServiceResolver
|
||||||
|
|
||||||
// TODO(rb): anything to normalize?
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -587,6 +576,9 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("Subset defined with empty name")
|
return fmt.Errorf("Subset defined with empty name")
|
||||||
}
|
}
|
||||||
|
if err := validateServiceSubset(name); err != nil {
|
||||||
|
return fmt.Errorf("Subset %q is invalid: %v", name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,12 +615,9 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
return fmt.Errorf("Redirect.Namespace defined without Redirect.Service")
|
return fmt.Errorf("Redirect.Namespace defined without Redirect.Service")
|
||||||
}
|
}
|
||||||
} else if r.Service == e.Name {
|
} else if r.Service == e.Name {
|
||||||
// TODO(rb): prevent self loops?
|
|
||||||
if r.ServiceSubset != "" && !isSubset(r.ServiceSubset) {
|
if r.ServiceSubset != "" && !isSubset(r.ServiceSubset) {
|
||||||
return fmt.Errorf("Redirect.ServiceSubset %q is not a valid subset of %q", r.ServiceSubset, r.Service)
|
return fmt.Errorf("Redirect.ServiceSubset %q is not a valid subset of %q", r.ServiceSubset, r.Service)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO(rb): handle validating subsets for other services
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -647,8 +636,6 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
if !isSubset(f.ServiceSubset) {
|
if !isSubset(f.ServiceSubset) {
|
||||||
return fmt.Errorf("Bad Failover[%q].ServiceSubset %q is not a valid subset of %q", subset, f.ServiceSubset, f.Service)
|
return fmt.Errorf("Bad Failover[%q].ServiceSubset %q is not a valid subset of %q", subset, f.ServiceSubset, f.Service)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO(rb): handle validating subsets for other services
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -656,8 +643,6 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
return fmt.Errorf("Bad Failover[%q].OverprovisioningFactor '%d', must be >= 0", subset, f.OverprovisioningFactor)
|
return fmt.Errorf("Bad Failover[%q].OverprovisioningFactor '%d', must be >= 0", subset, f.OverprovisioningFactor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(rb): more extensive validation will require graph traversal
|
|
||||||
|
|
||||||
for _, dc := range f.Datacenters {
|
for _, dc := range f.Datacenters {
|
||||||
if dc == "" {
|
if dc == "" {
|
||||||
return fmt.Errorf("Bad Failover[%q].Datacenters: found empty datacenter", subset)
|
return fmt.Errorf("Bad Failover[%q].Datacenters: found empty datacenter", subset)
|
||||||
|
@ -670,10 +655,6 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
return fmt.Errorf("Bad ConnectTimeout '%s', must be >= 0", e.ConnectTimeout)
|
return fmt.Errorf("Bad ConnectTimeout '%s', must be >= 0", e.ConnectTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(rb): validate the entire compiled chain? how?
|
|
||||||
|
|
||||||
// TODO(rb): validate more
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -710,6 +691,10 @@ func (e *ServiceResolverConfigEntry) ListRelatedServices() []string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(found) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
out := make([]string, 0, len(found))
|
out := make([]string, 0, len(found))
|
||||||
for svc, _ := range found {
|
for svc, _ := range found {
|
||||||
out = append(out, svc)
|
out = append(out, svc)
|
||||||
|
@ -1018,3 +1003,21 @@ func (e *ConfigEntryGraphError) Error() string {
|
||||||
}
|
}
|
||||||
return e.Message
|
return e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
validServiceSubset = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
|
||||||
|
serviceSubsetMaxLength = 63
|
||||||
|
)
|
||||||
|
|
||||||
|
// validateServiceSubset checks if the provided name can be used as an service
|
||||||
|
// subset. Because these are used in SNI headers they must a DNS label per
|
||||||
|
// RFC-1035/RFC-1123.
|
||||||
|
func validateServiceSubset(subset string) error {
|
||||||
|
if subset == "" || len(subset) > serviceSubsetMaxLength {
|
||||||
|
return fmt.Errorf("must be non-empty and 63 characters or fewer")
|
||||||
|
}
|
||||||
|
if !validServiceSubset.MatchString(subset) {
|
||||||
|
return fmt.Errorf("must be 63 characters or fewer, begin or end with lower case alphanumeric characters, and contain lower case alphanumeric characters or '-' in between")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,23 +1,265 @@
|
||||||
package structs
|
package structs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) {
|
||||||
|
// This test tests both of these because they are related functions.
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
newServiceACL := func(t *testing.T, canRead, canWrite []string) acl.Authorizer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, s := range canRead {
|
||||||
|
buf.WriteString(fmt.Sprintf("service %q { policy = %q }\n", s, "read"))
|
||||||
|
}
|
||||||
|
for _, s := range canWrite {
|
||||||
|
buf.WriteString(fmt.Sprintf("service %q { policy = %q }\n", s, "write"))
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, err := acl.NewPolicyFromSource("", 0, buf.String(), acl.SyntaxCurrent, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
authorizer, err := acl.NewPolicyAuthorizer(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
type testACL struct {
|
||||||
|
name string
|
||||||
|
authorizer acl.Authorizer
|
||||||
|
canRead bool
|
||||||
|
canWrite bool
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDenyCase := testACL{
|
||||||
|
name: "deny",
|
||||||
|
authorizer: newServiceACL(t, nil, nil),
|
||||||
|
canRead: false,
|
||||||
|
canWrite: false,
|
||||||
|
}
|
||||||
|
readTestCase := testACL{
|
||||||
|
name: "can read test",
|
||||||
|
authorizer: newServiceACL(t, []string{"test"}, nil),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: false,
|
||||||
|
}
|
||||||
|
writeTestCase := testACL{
|
||||||
|
name: "can write test",
|
||||||
|
authorizer: newServiceACL(t, nil, []string{"test"}),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: true,
|
||||||
|
}
|
||||||
|
writeTestCaseDenied := testACL{
|
||||||
|
name: "cannot write test",
|
||||||
|
authorizer: newServiceACL(t, nil, []string{"test"}),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
entry discoveryChainConfigEntry
|
||||||
|
expectServices []string
|
||||||
|
expectACLs []testACL
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "resolver: self",
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
|
Kind: ServiceResolver,
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
expectServices: nil,
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolver: redirect",
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
|
Kind: ServiceResolver,
|
||||||
|
Name: "test",
|
||||||
|
Redirect: &ServiceResolverRedirect{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectServices: []string{"other"},
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCaseDenied,
|
||||||
|
{
|
||||||
|
name: "can write test (with other:read)",
|
||||||
|
authorizer: newServiceACL(t, []string{"other"}, []string{"test"}),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolver: failover",
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
|
Kind: ServiceResolver,
|
||||||
|
Name: "test",
|
||||||
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
|
"foo": {OnlyPassing: true},
|
||||||
|
"bar": {OnlyPassing: true},
|
||||||
|
},
|
||||||
|
Failover: map[string]ServiceResolverFailover{
|
||||||
|
"foo": ServiceResolverFailover{
|
||||||
|
Service: "other1",
|
||||||
|
},
|
||||||
|
"bar": ServiceResolverFailover{
|
||||||
|
Service: "other2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectServices: []string{"other1", "other2"},
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCaseDenied,
|
||||||
|
{
|
||||||
|
name: "can write test (with other1:read and other2:read)",
|
||||||
|
authorizer: newServiceACL(t, []string{"other1", "other2"}, []string{"test"}),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "splitter: self",
|
||||||
|
entry: &ServiceSplitterConfigEntry{
|
||||||
|
Kind: ServiceSplitter,
|
||||||
|
Name: "test",
|
||||||
|
Splits: []ServiceSplit{
|
||||||
|
{Weight: 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectServices: nil,
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "splitter: some",
|
||||||
|
entry: &ServiceSplitterConfigEntry{
|
||||||
|
Kind: ServiceSplitter,
|
||||||
|
Name: "test",
|
||||||
|
Splits: []ServiceSplit{
|
||||||
|
{Weight: 25, Service: "b"},
|
||||||
|
{Weight: 25, Service: "a"},
|
||||||
|
{Weight: 50, Service: "c"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectServices: []string{"a", "b", "c"},
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCaseDenied,
|
||||||
|
{
|
||||||
|
name: "can write test (with a:read, b:read, and c:read)",
|
||||||
|
authorizer: newServiceACL(t, []string{"a", "b", "c"}, []string{"test"}),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "router: self",
|
||||||
|
entry: &ServiceRouterConfigEntry{
|
||||||
|
Kind: ServiceRouter,
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
expectServices: []string{"test"},
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "router: some",
|
||||||
|
entry: &ServiceRouterConfigEntry{
|
||||||
|
Kind: ServiceRouter,
|
||||||
|
Name: "test",
|
||||||
|
Routes: []ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &ServiceRouteMatch{HTTP: &ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/foo",
|
||||||
|
}},
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: &ServiceRouteMatch{HTTP: &ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/bar",
|
||||||
|
}},
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectServices: []string{"bar", "foo", "test"},
|
||||||
|
expectACLs: []testACL{
|
||||||
|
defaultDenyCase,
|
||||||
|
readTestCase,
|
||||||
|
writeTestCaseDenied,
|
||||||
|
{
|
||||||
|
name: "can write test (with foo:read and bar:read)",
|
||||||
|
authorizer: newServiceACL(t, []string{"foo", "bar"}, []string{"test"}),
|
||||||
|
canRead: true,
|
||||||
|
canWrite: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// sanity check inputs
|
||||||
|
require.NoError(t, tc.entry.Normalize())
|
||||||
|
require.NoError(t, tc.entry.Validate())
|
||||||
|
|
||||||
|
got := tc.entry.ListRelatedServices()
|
||||||
|
require.Equal(t, tc.expectServices, got)
|
||||||
|
|
||||||
|
for _, a := range tc.expectACLs {
|
||||||
|
a := a
|
||||||
|
t.Run(a.name, func(t *testing.T) {
|
||||||
|
require.Equal(t, a.canRead, tc.entry.CanRead(a.authorizer))
|
||||||
|
require.Equal(t, a.canWrite, tc.entry.CanWrite(a.authorizer))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServiceResolverConfigEntry(t *testing.T) {
|
func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
entry *ServiceResolverConfigEntry
|
entry *ServiceResolverConfigEntry
|
||||||
normalizeErr string
|
normalizeErr string
|
||||||
validateErr string
|
validateErr string
|
||||||
// check is called between normalize and validate
|
// check is called between normalize and validate
|
||||||
check func(t *testing.T, entry *ServiceResolverConfigEntry)
|
check func(t *testing.T, entry *ServiceResolverConfigEntry)
|
||||||
}{
|
}
|
||||||
|
|
||||||
|
cases := []testcase{
|
||||||
{
|
{
|
||||||
name: "nil",
|
name: "nil",
|
||||||
entry: nil,
|
entry: nil,
|
||||||
|
@ -251,7 +493,39 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
},
|
},
|
||||||
validateErr: "Bad ConnectTimeout",
|
validateErr: "Bad ConnectTimeout",
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
// Bulk add a bunch of similar validation cases.
|
||||||
|
for _, invalidSubset := range invalidSubsetNames {
|
||||||
|
tc := testcase{
|
||||||
|
name: "invalid subset name: " + invalidSubset,
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
|
Kind: ServiceResolver,
|
||||||
|
Name: "test",
|
||||||
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
|
invalidSubset: {OnlyPassing: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: fmt.Sprintf("Subset %q is invalid", invalidSubset),
|
||||||
|
}
|
||||||
|
cases = append(cases, tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, goodSubset := range validSubsetNames {
|
||||||
|
tc := testcase{
|
||||||
|
name: "valid subset name: " + goodSubset,
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
|
Kind: ServiceResolver,
|
||||||
|
Name: "test",
|
||||||
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
|
goodSubset: {OnlyPassing: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cases = append(cases, tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
err := tc.entry.Normalize()
|
err := tc.entry.Normalize()
|
||||||
|
@ -461,3 +735,369 @@ func TestServiceSplitterConfigEntry(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceRouterConfigEntry(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
httpMatch := func(http *ServiceRouteHTTPMatch) *ServiceRouteMatch {
|
||||||
|
return &ServiceRouteMatch{HTTP: http}
|
||||||
|
}
|
||||||
|
httpMatchHeader := func(headers ...ServiceRouteHTTPMatchHeader) *ServiceRouteMatch {
|
||||||
|
return httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
Header: headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
httpMatchParam := func(params ...ServiceRouteHTTPMatchQueryParam) *ServiceRouteMatch {
|
||||||
|
return httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
QueryParam: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toService := func(svc string) *ServiceRouteDestination {
|
||||||
|
return &ServiceRouteDestination{Service: svc}
|
||||||
|
}
|
||||||
|
routeMatch := func(match *ServiceRouteMatch) ServiceRoute {
|
||||||
|
return ServiceRoute{
|
||||||
|
Match: match,
|
||||||
|
Destination: toService("other"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
makerouter := func(routes ...ServiceRoute) *ServiceRouterConfigEntry {
|
||||||
|
return &ServiceRouterConfigEntry{
|
||||||
|
Kind: ServiceRouter,
|
||||||
|
Name: "test",
|
||||||
|
Routes: routes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testcase struct {
|
||||||
|
name string
|
||||||
|
entry *ServiceRouterConfigEntry
|
||||||
|
normalizeErr string
|
||||||
|
validateErr string
|
||||||
|
// check is called between normalize and validate
|
||||||
|
check func(t *testing.T, entry *ServiceRouterConfigEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testcase{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
entry: nil,
|
||||||
|
normalizeErr: "config entry is nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no name",
|
||||||
|
entry: &ServiceRouterConfigEntry{},
|
||||||
|
validateErr: "Name is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
entry: makerouter(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1 empty route",
|
||||||
|
entry: makerouter(
|
||||||
|
ServiceRoute{},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "route with path exact",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/exact",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with bad path exact",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "no-leading-slash",
|
||||||
|
}))),
|
||||||
|
validateErr: "PathExact doesn't start with '/'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path prefix",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/prefix",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with bad path prefix",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "no-leading-slash",
|
||||||
|
}))),
|
||||||
|
validateErr: "PathPrefix doesn't start with '/'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path regex",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathRegex: "/regex",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path exact and prefix",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/exact",
|
||||||
|
PathPrefix: "/prefix",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path exact and regex",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/exact",
|
||||||
|
PathRegex: "/regex",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path prefix and regex",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/prefix",
|
||||||
|
PathRegex: "/regex",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path exact, prefix, and regex",
|
||||||
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/exact",
|
||||||
|
PathPrefix: "/prefix",
|
||||||
|
PathRegex: "/regex",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "route with no name header",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Present: true,
|
||||||
|
}))),
|
||||||
|
validateErr: "missing required Name field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header present",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Present: true,
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header not present",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Present: true,
|
||||||
|
Invert: true,
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header exact",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Exact: "bar",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header regex",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Regex: "bar",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header prefix",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Prefix: "bar",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header suffix",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Suffix: "bar",
|
||||||
|
}))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header present and exact",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Present: true,
|
||||||
|
Exact: "bar",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header present and regex",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Present: true,
|
||||||
|
Regex: "bar",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header present and prefix",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Present: true,
|
||||||
|
Prefix: "bar",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header present and suffix",
|
||||||
|
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Present: true,
|
||||||
|
Suffix: "bar",
|
||||||
|
}))),
|
||||||
|
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
|
||||||
|
},
|
||||||
|
// NOTE: Some combinatoric cases for header operators (some 5 choose 2,
|
||||||
|
// all 5 choose 3, all 5 choose 4, all 5 choose 5) are omitted from
|
||||||
|
// testing.
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
{
|
||||||
|
name: "route with no name query param",
|
||||||
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
|
||||||
|
Value: "foo",
|
||||||
|
}))),
|
||||||
|
validateErr: "missing required Name field",
|
||||||
|
},
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
{
|
||||||
|
name: "route with no match and prefix rewrite",
|
||||||
|
entry: makerouter(ServiceRoute{
|
||||||
|
Match: nil,
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
PrefixRewrite: "/new",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path prefix match and prefix rewrite",
|
||||||
|
entry: makerouter(ServiceRoute{
|
||||||
|
Match: httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/api",
|
||||||
|
}),
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
PrefixRewrite: "/new",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path exact match and prefix rewrite",
|
||||||
|
entry: makerouter(ServiceRoute{
|
||||||
|
Match: httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/api",
|
||||||
|
}),
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
PrefixRewrite: "/new",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with path regex match and prefix rewrite",
|
||||||
|
entry: makerouter(ServiceRoute{
|
||||||
|
Match: httpMatch(&ServiceRouteHTTPMatch{
|
||||||
|
PathRegex: "/api",
|
||||||
|
}),
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
PrefixRewrite: "/new",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header match and prefix rewrite",
|
||||||
|
entry: makerouter(ServiceRoute{
|
||||||
|
Match: httpMatchHeader(ServiceRouteHTTPMatchHeader{
|
||||||
|
Name: "foo",
|
||||||
|
Exact: "bar",
|
||||||
|
}),
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
PrefixRewrite: "/new",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route with header match and prefix rewrite",
|
||||||
|
entry: makerouter(ServiceRoute{
|
||||||
|
Match: httpMatchParam(ServiceRouteHTTPMatchQueryParam{
|
||||||
|
Name: "foo",
|
||||||
|
Value: "bar",
|
||||||
|
}),
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
PrefixRewrite: "/new",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.entry.Normalize()
|
||||||
|
if tc.normalizeErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.normalizeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if tc.check != nil {
|
||||||
|
tc.check(t, tc.entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.entry.Validate()
|
||||||
|
if tc.validateErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.validateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var validSubsetNames = []string{
|
||||||
|
"a", "aa", "2a", "a2", "a2a", "a22a",
|
||||||
|
"1", "11", "10", "01",
|
||||||
|
"a-a", "a--a", "a--a--a",
|
||||||
|
"0-0", "0--0", "0--0--0",
|
||||||
|
strings.Repeat("a", 63),
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidSubsetNames = []string{
|
||||||
|
"A", "AA", "2A", "A2", "A2A", "A22A",
|
||||||
|
"A-A", "A--A", "A--A--A",
|
||||||
|
" ", " a", "a ", "a a",
|
||||||
|
"_", "_a", "a_", "a_a",
|
||||||
|
".", ".a", "a.", "a.a",
|
||||||
|
"-", "-a", "a-",
|
||||||
|
strings.Repeat("a", 64),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateServiceSubset(t *testing.T) {
|
||||||
|
for _, name := range validSubsetNames {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
require.NoError(t, validateServiceSubset(name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range invalidSubsetNames {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
require.Error(t, validateServiceSubset(name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -240,12 +240,6 @@ func makeRouteMatchForDiscoveryRoute(discoveryRoute *structs.DiscoveryRoute, pro
|
||||||
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{
|
||||||
PresentMatch: true,
|
PresentMatch: true,
|
||||||
}
|
}
|
||||||
case hdr.Invert: // THIS HAS TO BE LAST
|
|
||||||
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{
|
|
||||||
// We set this to the misleading value of 'true' here
|
|
||||||
// because we'll generically invert it next.
|
|
||||||
PresentMatch: true,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
continue // skip this impossible situation
|
continue // skip this impossible situation
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,8 +156,9 @@ func TestRoutesFromSnapshot(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Match: httpMatchHeader(structs.ServiceRouteHTTPMatchHeader{
|
Match: httpMatchHeader(structs.ServiceRouteHTTPMatchHeader{
|
||||||
Name: "x-debug",
|
Name: "x-debug",
|
||||||
Invert: true,
|
Present: true,
|
||||||
|
Invert: true,
|
||||||
}),
|
}),
|
||||||
Destination: toService("hdr-not-present"),
|
Destination: toService("hdr-not-present"),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue