mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2845 lines
72 KiB
2845 lines
72 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package structs |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"strings" |
|
"testing" |
|
"time" |
|
|
|
"github.com/stretchr/testify/assert" |
|
"github.com/stretchr/testify/require" |
|
|
|
"github.com/hashicorp/consul/acl" |
|
) |
|
|
|
func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { |
|
// This test tests both of these because they are related functions. |
|
|
|
newAuthz := func(t *testing.T, src string) acl.Authorizer { |
|
policy, err := acl.NewPolicyFromSource(src, nil, nil) |
|
require.NoError(t, err) |
|
|
|
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil) |
|
require.NoError(t, err) |
|
return authorizer |
|
} |
|
|
|
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(buf.String(), nil, nil) |
|
require.NoError(t, err) |
|
|
|
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil) |
|
require.NoError(t, err) |
|
return authorizer |
|
} |
|
|
|
newServiceAndOperatorACL := func(t *testing.T, service, operator string) acl.Authorizer { |
|
switch { |
|
case service != "" && operator != "": |
|
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q } operator = %q`, service, operator)) |
|
case service == "" && operator != "": |
|
return newAuthz(t, fmt.Sprintf(`operator = %q`, operator)) |
|
case service != "" && operator == "": |
|
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q }`, service)) |
|
default: |
|
t.Fatalf("one of these should be set") |
|
return nil |
|
} |
|
} |
|
|
|
newServiceAndMeshACL := func(t *testing.T, service, mesh string) acl.Authorizer { |
|
switch { |
|
case service != "" && mesh != "": |
|
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q } mesh = %q`, service, mesh)) |
|
case service == "" && mesh != "": |
|
return newAuthz(t, fmt.Sprintf(`mesh = %q`, mesh)) |
|
case service != "" && mesh == "": |
|
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q }`, service)) |
|
default: |
|
t.Fatalf("one of these should be set") |
|
return nil |
|
} |
|
} |
|
|
|
type testACL = configEntryTestACL |
|
type testcase = configEntryACLTestCase |
|
|
|
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, |
|
} |
|
|
|
cases := []testcase{ |
|
{ |
|
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: []ServiceID{NewServiceID("other", nil)}, |
|
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": { |
|
Service: "other1", |
|
}, |
|
"bar": { |
|
Service: "other2", |
|
}, |
|
}, |
|
}, |
|
expectServices: []ServiceID{NewServiceID("other1", nil), NewServiceID("other2", nil)}, |
|
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: "resolver: failover with targets", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Targets: []ServiceResolverFailoverTarget{ |
|
{Service: "other1"}, |
|
{Datacenter: "dc2"}, |
|
{Peer: "cluster-01"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
expectServices: []ServiceID{NewServiceID("other1", nil)}, |
|
expectACLs: []testACL{ |
|
defaultDenyCase, |
|
readTestCase, |
|
writeTestCaseDenied, |
|
{ |
|
name: "can write test (with other1:read)", |
|
authorizer: newServiceACL(t, []string{"other1"}, []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: []ServiceID{NewServiceID("a", nil), NewServiceID("b", nil), NewServiceID("c", nil)}, |
|
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: []ServiceID{NewServiceID("test", nil)}, |
|
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: []ServiceID{NewServiceID("bar", nil), NewServiceID("foo", nil), NewServiceID("test", nil)}, |
|
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, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "ingress-gateway", |
|
entry: &IngressGatewayConfigEntry{Name: "test"}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "api-gateway", |
|
entry: &APIGatewayConfigEntry{ |
|
Name: "test", |
|
Listeners: []APIGatewayListener{ |
|
{ |
|
Name: "test", |
|
Port: 100, |
|
Protocol: "http", |
|
}, |
|
}, |
|
}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "inline-certificate", |
|
entry: &InlineCertificateConfigEntry{Name: "test", Certificate: validCertificate, PrivateKey: validPrivateKey}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "http-route", |
|
entry: &HTTPRouteConfigEntry{Name: "test"}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "tcp-route", |
|
entry: &TCPRouteConfigEntry{Name: "test"}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "bound-api-gateway", |
|
entry: &BoundAPIGatewayConfigEntry{Name: "test"}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "terminating-gateway", |
|
entry: &TerminatingGatewayConfigEntry{Name: "test"}, |
|
expectACLs: []testACL{ |
|
{ |
|
name: "no-authz", |
|
authorizer: newAuthz(t, ``), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator deny", |
|
authorizer: newServiceAndOperatorACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "deny", "deny"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "read", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh deny", |
|
authorizer: newServiceAndMeshACL(t, "write", "deny"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and operator read", |
|
authorizer: newServiceAndOperatorACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and operator write", |
|
authorizer: newServiceAndOperatorACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "deny", "read"), |
|
canRead: false, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service read and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "read", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
{ |
|
name: "service write and mesh read", |
|
authorizer: newServiceAndMeshACL(t, "write", "read"), |
|
canRead: true, |
|
canWrite: false, |
|
}, |
|
|
|
{ |
|
name: "service deny and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "deny", "write"), |
|
canRead: false, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service read and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "read", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
{ |
|
name: "service write and mesh write", |
|
authorizer: newServiceAndMeshACL(t, "write", "write"), |
|
canRead: true, |
|
canWrite: true, |
|
}, |
|
}, |
|
}, |
|
} |
|
|
|
testConfigEntries_ListRelatedServices_AndACLs(t, cases) |
|
} |
|
|
|
func TestServiceResolverConfigEntry(t *testing.T) { |
|
|
|
type testcase struct { |
|
name string |
|
entry *ServiceResolverConfigEntry |
|
normalizeErr string |
|
validateErr string |
|
// check is called between normalize and validate |
|
check func(t *testing.T, entry *ServiceResolverConfigEntry) |
|
} |
|
|
|
cases := []testcase{ |
|
{ |
|
name: "nil", |
|
entry: nil, |
|
normalizeErr: "config entry is nil", |
|
}, |
|
{ |
|
name: "no name", |
|
entry: &ServiceResolverConfigEntry{}, |
|
validateErr: "Name is required", |
|
}, |
|
{ |
|
name: "empty", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
}, |
|
}, |
|
{ |
|
name: "empty subset name", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"": {OnlyPassing: true}, |
|
}, |
|
}, |
|
validateErr: "Subset defined with empty name", |
|
}, |
|
{ |
|
name: "invalid boolean expression subset filter", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "random string"}, |
|
}, |
|
}, |
|
validateErr: `Filter for subset "v1" is not a valid expression`, |
|
}, |
|
{ |
|
name: "default subset does not exist", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
DefaultSubset: "gone", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
}, |
|
}, |
|
validateErr: `DefaultSubset "gone" is not a valid subset`, |
|
}, |
|
{ |
|
name: "default subset does exist", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
DefaultSubset: "v1", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "empty redirect", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{}, |
|
}, |
|
validateErr: "Redirect is empty", |
|
}, |
|
{ |
|
name: "empty redirect", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{}, |
|
}, |
|
validateErr: "Redirect is empty", |
|
}, |
|
{ |
|
name: "redirect subset with no service", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
ServiceSubset: "next", |
|
}, |
|
}, |
|
validateErr: "Redirect.ServiceSubset defined without Redirect.Service", |
|
}, |
|
{ |
|
name: "self redirect with invalid subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
Service: "test", |
|
ServiceSubset: "gone", |
|
}, |
|
}, |
|
validateErr: `Redirect.ServiceSubset "gone" is not a valid subset of "test"`, |
|
}, |
|
{ |
|
name: "redirect with peer and subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
Peer: "cluster-01", |
|
ServiceSubset: "gone", |
|
}, |
|
}, |
|
validateErr: `Redirect.Peer cannot be set with Redirect.ServiceSubset`, |
|
}, |
|
{ |
|
name: "redirect with peer and datacenter", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
Peer: "cluster-01", |
|
Datacenter: "dc2", |
|
}, |
|
}, |
|
validateErr: `Redirect.Peer cannot be set with Redirect.Datacenter`, |
|
}, |
|
{ |
|
name: "redirect with peer and datacenter", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
Peer: "cluster-01", |
|
}, |
|
}, |
|
validateErr: `Redirect.Peer defined without Redirect.Service`, |
|
}, |
|
{ |
|
name: "self redirect with valid subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
Service: "test", |
|
ServiceSubset: "v1", |
|
}, |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "redirect to peer", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Redirect: &ServiceResolverRedirect{ |
|
Service: "other", |
|
Peer: "cluster-01", |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "simple wildcard failover", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Datacenters: []string{"dc2"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "failover for missing subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"gone": { |
|
Datacenters: []string{"dc2"}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["gone"]: not a valid subset`, |
|
}, |
|
{ |
|
name: "failover for present subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
}, |
|
Failover: map[string]ServiceResolverFailover{ |
|
"v1": { |
|
Datacenters: []string{"dc2"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "failover empty", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
}, |
|
Failover: map[string]ServiceResolverFailover{ |
|
"v1": {}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["v1"]: one of Service, ServiceSubset, Namespace, Targets, SamenessGroup, or Datacenters is required`, |
|
}, |
|
{ |
|
name: "failover to self using invalid subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
}, |
|
Failover: map[string]ServiceResolverFailover{ |
|
"v1": { |
|
Service: "test", |
|
ServiceSubset: "gone", |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["v1"]: ServiceSubset "gone" is not a valid subset of "test"`, |
|
}, |
|
{ |
|
name: "failover to self using valid subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": {Filter: "Service.Meta.version == v1"}, |
|
"v2": {Filter: "Service.Meta.version == v2"}, |
|
}, |
|
Failover: map[string]ServiceResolverFailover{ |
|
"v1": { |
|
Service: "test", |
|
ServiceSubset: "v2", |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "failover with empty datacenters in list", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Service: "backup", |
|
Datacenters: []string{"", "dc2", "dc3"}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"].Datacenters: found empty datacenter`, |
|
}, |
|
{ |
|
name: "failover target with an invalid subset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Targets: []ServiceResolverFailoverTarget{{ServiceSubset: "subset"}}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"].Targets[0]: ServiceSubset "subset" is not a valid subset of "test"`, |
|
}, |
|
{ |
|
name: "failover targets can't have Peer and ServiceSubset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01", ServiceSubset: "subset"}}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"].Targets[0]: Peer cannot be set with ServiceSubset`, |
|
}, |
|
{ |
|
name: "failover targets can't have Peer and Datacenter", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01", Datacenter: "dc1"}}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"].Targets[0]: Peer cannot be set with Datacenter`, |
|
}, |
|
{ |
|
name: "failover Targets cannot be set with Datacenters", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Datacenters: []string{"a"}, |
|
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"]: Targets cannot be set with Datacenters`, |
|
}, |
|
{ |
|
name: "failover Targets cannot be set with ServiceSubset", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
ServiceSubset: "v2", |
|
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}}, |
|
}, |
|
}, |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v2": {Filter: "Service.Meta.version == v2"}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"]: Targets cannot be set with ServiceSubset`, |
|
}, |
|
{ |
|
name: "failover Targets cannot be set with Service", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Service: "another-service", |
|
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}}, |
|
}, |
|
}, |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v2": {Filter: "Service.Meta.version == v2"}, |
|
}, |
|
}, |
|
validateErr: `Bad Failover["*"]: Targets cannot be set with Service`, |
|
}, |
|
{ |
|
name: "complicated failover targets", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
Failover: map[string]ServiceResolverFailover{ |
|
"*": { |
|
Targets: []ServiceResolverFailoverTarget{ |
|
{Peer: "cluster-01", Service: "test-v2"}, |
|
{Service: "test-v2", ServiceSubset: "test"}, |
|
{Datacenter: "dc2"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "bad connect timeout", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
ConnectTimeout: -1 * time.Second, |
|
}, |
|
validateErr: "Bad ConnectTimeout", |
|
}, |
|
{ |
|
name: "bad request timeout", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
RequestTimeout: -1 * time.Second, |
|
}, |
|
validateErr: "Bad RequestTimeout", |
|
}, |
|
} |
|
|
|
// 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 |
|
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) |
|
}) |
|
} |
|
} |
|
|
|
func TestServiceResolverConfigEntry_LoadBalancer(t *testing.T) { |
|
|
|
type testcase struct { |
|
name string |
|
entry *ServiceResolverConfigEntry |
|
normalizeErr string |
|
validateErr string |
|
|
|
// check is called between normalize and validate |
|
check func(t *testing.T, entry *ServiceResolverConfigEntry) |
|
} |
|
|
|
cases := []testcase{ |
|
{ |
|
name: "empty policy is valid", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: "", |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "supported policy", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyRandom, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "unsupported policy", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: "fake-policy", |
|
}, |
|
}, |
|
validateErr: `"fake-policy" is not supported`, |
|
}, |
|
{ |
|
name: "bad policy for least request config", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyRingHash, |
|
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10}, |
|
}, |
|
}, |
|
validateErr: `LeastRequestConfig specified for incompatible load balancing policy`, |
|
}, |
|
{ |
|
name: "bad policy for ring hash config", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyLeastRequest, |
|
RingHashConfig: &RingHashConfig{MinimumRingSize: 1024}, |
|
}, |
|
}, |
|
validateErr: `RingHashConfig specified for incompatible load balancing policy`, |
|
}, |
|
{ |
|
name: "good policy for ring hash config", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyRingHash, |
|
RingHashConfig: &RingHashConfig{MinimumRingSize: 1024}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "good policy for least request config", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyLeastRequest, |
|
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 2}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "empty policy is not defaulted", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: "", |
|
}, |
|
}, |
|
check: func(t *testing.T, entry *ServiceResolverConfigEntry) { |
|
require.Equal(t, "", entry.LoadBalancer.Policy) |
|
}, |
|
}, |
|
{ |
|
name: "empty policy with hash policy", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: "", |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
SourceIP: true, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `HashPolicies specified for non-hash-based Policy`, |
|
}, |
|
{ |
|
name: "cookie config with header policy", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: HashPolicyHeader, |
|
FieldValue: "x-user-id", |
|
CookieConfig: &CookieConfig{ |
|
TTL: 10 * time.Second, |
|
Path: "/root", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `cookie_config provided for "header"`, |
|
}, |
|
{ |
|
name: "cannot generate session cookie with ttl", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: HashPolicyCookie, |
|
FieldValue: "good-cookie", |
|
CookieConfig: &CookieConfig{ |
|
Session: true, |
|
TTL: 10 * time.Second, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `a session cookie cannot have an associated TTL`, |
|
}, |
|
{ |
|
name: "valid cookie policy", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: HashPolicyCookie, |
|
FieldValue: "good-cookie", |
|
CookieConfig: &CookieConfig{ |
|
TTL: 10 * time.Second, |
|
Path: "/oven", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "supported match field", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: "header", |
|
FieldValue: "X-Consul-Token", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "unsupported match field", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: "fake-field", |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `"fake-field" is not a supported field`, |
|
}, |
|
{ |
|
name: "cannot match on source address and custom field", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: "header", |
|
SourceIP: true, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `A single hash policy cannot hash both a source address and a "header"`, |
|
}, |
|
{ |
|
name: "matchvalue not compatible with source address", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
FieldValue: "X-Consul-Token", |
|
SourceIP: true, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `A FieldValue cannot be specified when hashing SourceIP`, |
|
}, |
|
{ |
|
name: "field without match value", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: "header", |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Field "header" was specified without a FieldValue`, |
|
}, |
|
{ |
|
name: "field without match value", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyMaglev, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
FieldValue: "my-cookie", |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `FieldValue requires a Field to apply to`, |
|
}, |
|
{ |
|
name: "ring hash kitchen sink", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyRingHash, |
|
RingHashConfig: &RingHashConfig{MaximumRingSize: 10, MinimumRingSize: 2}, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: "cookie", |
|
FieldValue: "my-cookie", |
|
}, |
|
{ |
|
Field: "header", |
|
FieldValue: "alt-header", |
|
Terminal: true, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "least request kitchen sink", |
|
entry: &ServiceResolverConfigEntry{ |
|
Kind: ServiceResolver, |
|
Name: "test", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: LBPolicyLeastRequest, |
|
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 20}, |
|
}, |
|
}, |
|
}, |
|
} |
|
|
|
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) |
|
}) |
|
} |
|
} |
|
|
|
func TestServiceSplitterConfigEntry(t *testing.T) { |
|
|
|
makesplitter := func(splits ...ServiceSplit) *ServiceSplitterConfigEntry { |
|
return &ServiceSplitterConfigEntry{ |
|
Kind: ServiceSplitter, |
|
Name: "test", |
|
Splits: splits, |
|
} |
|
} |
|
|
|
makesplit := func(weight float32, service, serviceSubset, namespace string) ServiceSplit { |
|
return ServiceSplit{ |
|
Weight: weight, |
|
Service: service, |
|
ServiceSubset: serviceSubset, |
|
Namespace: namespace, |
|
} |
|
} |
|
|
|
for _, tc := range []struct { |
|
name string |
|
entry *ServiceSplitterConfigEntry |
|
normalizeErr string |
|
validateErr string |
|
// check is called between normalize and validate |
|
check func(t *testing.T, entry *ServiceSplitterConfigEntry) |
|
}{ |
|
{ |
|
name: "nil", |
|
entry: nil, |
|
normalizeErr: "config entry is nil", |
|
}, |
|
{ |
|
name: "no name", |
|
entry: &ServiceSplitterConfigEntry{}, |
|
validateErr: "Name is required", |
|
}, |
|
{ |
|
name: "empty", |
|
entry: makesplitter(), |
|
validateErr: "no splits configured", |
|
}, |
|
{ |
|
name: "1 split", |
|
entry: makesplitter( |
|
makesplit(100, "test", "", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(100), entry.Splits[0].Weight) |
|
}, |
|
}, |
|
{ |
|
name: "1 split not enough weight", |
|
entry: makesplitter( |
|
makesplit(99.99, "test", "", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(99.99), entry.Splits[0].Weight) |
|
}, |
|
validateErr: "the sum of all split weights must be 100", |
|
}, |
|
{ |
|
name: "1 split too much weight", |
|
entry: makesplitter( |
|
makesplit(100.01, "test", "", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(100.01), entry.Splits[0].Weight) |
|
}, |
|
validateErr: "the sum of all split weights must be 100", |
|
}, |
|
{ |
|
name: "2 splits", |
|
entry: makesplitter( |
|
makesplit(99, "test", "v1", ""), |
|
makesplit(1, "test", "v2", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(99), entry.Splits[0].Weight) |
|
require.Equal(t, float32(1), entry.Splits[1].Weight) |
|
}, |
|
}, |
|
{ |
|
name: "2 splits - rounded up to smallest units", |
|
entry: makesplitter( |
|
makesplit(99.999, "test", "v1", ""), |
|
makesplit(0.001, "test", "v2", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(100), entry.Splits[0].Weight) |
|
require.Equal(t, float32(0), entry.Splits[1].Weight) |
|
}, |
|
}, |
|
{ |
|
name: "2 splits not enough weight", |
|
entry: makesplitter( |
|
makesplit(99.98, "test", "v1", ""), |
|
makesplit(0.01, "test", "v2", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(99.98), entry.Splits[0].Weight) |
|
require.Equal(t, float32(0.01), entry.Splits[1].Weight) |
|
}, |
|
validateErr: "the sum of all split weights must be 100", |
|
}, |
|
{ |
|
name: "2 splits too much weight", |
|
entry: makesplitter( |
|
makesplit(100, "test", "v1", ""), |
|
makesplit(0.01, "test", "v2", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(100), entry.Splits[0].Weight) |
|
require.Equal(t, float32(0.01), entry.Splits[1].Weight) |
|
}, |
|
validateErr: "the sum of all split weights must be 100", |
|
}, |
|
{ |
|
name: "3 splits", |
|
entry: makesplitter( |
|
makesplit(34, "test", "v1", ""), |
|
makesplit(33, "test", "v2", ""), |
|
makesplit(33, "test", "v3", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(34), entry.Splits[0].Weight) |
|
require.Equal(t, float32(33), entry.Splits[1].Weight) |
|
require.Equal(t, float32(33), entry.Splits[2].Weight) |
|
}, |
|
}, |
|
{ |
|
name: "3 splits one duplicated same weights", |
|
entry: makesplitter( |
|
makesplit(34, "test", "v1", ""), |
|
makesplit(33, "test", "v2", ""), |
|
makesplit(33, "test", "v2", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(34), entry.Splits[0].Weight) |
|
require.Equal(t, float32(33), entry.Splits[1].Weight) |
|
require.Equal(t, float32(33), entry.Splits[2].Weight) |
|
}, |
|
validateErr: "split destination occurs more than once", |
|
}, |
|
{ |
|
name: "3 splits one duplicated diff weights", |
|
entry: makesplitter( |
|
makesplit(34, "test", "v1", ""), |
|
makesplit(33, "test", "v2", ""), |
|
makesplit(33, "test", "v1", ""), |
|
), |
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) { |
|
require.Equal(t, float32(34), entry.Splits[0].Weight) |
|
require.Equal(t, float32(33), entry.Splits[1].Weight) |
|
require.Equal(t, float32(33), entry.Splits[2].Weight) |
|
}, |
|
validateErr: "split destination occurs more than once", |
|
}, |
|
} { |
|
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) |
|
}) |
|
} |
|
} |
|
|
|
func TestServiceSplitMergeParent(t *testing.T) { |
|
|
|
type testCase struct { |
|
name string |
|
split, parent, want *ServiceSplit |
|
wantErr string |
|
} |
|
|
|
run := func(t *testing.T, tc testCase) { |
|
got, err := tc.split.MergeParent(tc.parent) |
|
if tc.wantErr != "" { |
|
require.Error(t, err) |
|
require.Contains(t, err.Error(), tc.wantErr) |
|
} else { |
|
require.NoError(t, err) |
|
require.Equal(t, tc.want, got) |
|
} |
|
} |
|
|
|
testCases := []testCase{ |
|
{ |
|
name: "all header manip fields set", |
|
split: &ServiceSplit{ |
|
Weight: 50.0, |
|
Service: "foo", |
|
RequestHeaders: &HTTPHeaderModifiers{ |
|
Add: map[string]string{ |
|
"child-only": "1", |
|
"both-want-child": "2", |
|
}, |
|
Set: map[string]string{ |
|
"child-only": "3", |
|
"both-want-child": "4", |
|
}, |
|
Remove: []string{"child-only-req", "both-req"}, |
|
}, |
|
ResponseHeaders: &HTTPHeaderModifiers{ |
|
Add: map[string]string{ |
|
"child-only": "5", |
|
"both-want-parent": "6", |
|
}, |
|
Set: map[string]string{ |
|
"child-only": "7", |
|
"both-want-parent": "8", |
|
}, |
|
Remove: []string{"child-only-resp", "both-resp"}, |
|
}, |
|
}, |
|
parent: &ServiceSplit{ |
|
Weight: 25.0, |
|
Service: "bar", |
|
RequestHeaders: &HTTPHeaderModifiers{ |
|
Add: map[string]string{ |
|
"parent-only": "9", |
|
"both-want-child": "10", |
|
}, |
|
Set: map[string]string{ |
|
"parent-only": "11", |
|
"both-want-child": "12", |
|
}, |
|
Remove: []string{"parent-only-req", "both-req"}, |
|
}, |
|
ResponseHeaders: &HTTPHeaderModifiers{ |
|
Add: map[string]string{ |
|
"parent-only": "13", |
|
"both-want-parent": "14", |
|
}, |
|
Set: map[string]string{ |
|
"parent-only": "15", |
|
"both-want-parent": "16", |
|
}, |
|
Remove: []string{"parent-only-resp", "both-resp"}, |
|
}, |
|
}, |
|
want: &ServiceSplit{ |
|
Weight: 50.0, |
|
Service: "foo", |
|
RequestHeaders: &HTTPHeaderModifiers{ |
|
Add: map[string]string{ |
|
"child-only": "1", |
|
"both-want-child": "2", |
|
"parent-only": "9", |
|
}, |
|
Set: map[string]string{ |
|
"child-only": "3", |
|
"both-want-child": "4", |
|
"parent-only": "11", |
|
}, |
|
Remove: []string{"parent-only-req", "both-req", "child-only-req"}, |
|
}, |
|
ResponseHeaders: &HTTPHeaderModifiers{ |
|
Add: map[string]string{ |
|
"child-only": "5", |
|
"parent-only": "13", |
|
"both-want-parent": "14", |
|
}, |
|
Set: map[string]string{ |
|
"child-only": "7", |
|
"parent-only": "15", |
|
"both-want-parent": "16", |
|
}, |
|
Remove: []string{"child-only-resp", "both-resp", "parent-only-resp"}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "no header manip", |
|
split: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "foo", |
|
}, |
|
parent: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "bar", |
|
}, |
|
want: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "foo", |
|
}, |
|
}, |
|
{ |
|
name: "nil parent", |
|
split: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "foo", |
|
}, |
|
parent: nil, |
|
want: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "foo", |
|
}, |
|
}, |
|
{ |
|
name: "nil child", |
|
split: nil, |
|
parent: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "foo", |
|
}, |
|
want: &ServiceSplit{ |
|
Weight: 50, |
|
Service: "foo", |
|
}, |
|
}, |
|
{ |
|
name: "both nil", |
|
split: nil, |
|
parent: nil, |
|
want: nil, |
|
}, |
|
} |
|
|
|
for _, tc := range testCases { |
|
t.Run(tc.name, func(t *testing.T) { |
|
run(t, tc) |
|
}) |
|
} |
|
} |
|
|
|
func TestServiceRouterConfigEntry(t *testing.T) { |
|
|
|
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{ |
|
Exact: "foo", |
|
}))), |
|
validateErr: "missing required Name field", |
|
}, |
|
{ |
|
name: "route with query param exact match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Exact: "bar", |
|
}))), |
|
}, |
|
{ |
|
name: "route with query param regex match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Regex: "bar", |
|
}))), |
|
}, |
|
{ |
|
name: "route with query param present match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Present: true, |
|
}))), |
|
}, |
|
{ |
|
name: "route with query param exact and regex match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Exact: "bar", |
|
Regex: "bar", |
|
}))), |
|
validateErr: "should only contain one of Present, Exact, or Regex", |
|
}, |
|
{ |
|
name: "route with query param exact and present match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Exact: "bar", |
|
Present: true, |
|
}))), |
|
validateErr: "should only contain one of Present, Exact, or Regex", |
|
}, |
|
{ |
|
name: "route with query param regex and present match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Regex: "bar", |
|
Present: true, |
|
}))), |
|
validateErr: "should only contain one of Present, Exact, or Regex", |
|
}, |
|
{ |
|
name: "route with query param exact, regex, and present match", |
|
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ |
|
Name: "foo", |
|
Exact: "bar", |
|
Regex: "bar", |
|
Present: true, |
|
}))), |
|
validateErr: "should only contain one of Present, Exact, or Regex", |
|
}, |
|
//////////////// |
|
{ |
|
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", |
|
Exact: "bar", |
|
}), |
|
Destination: &ServiceRouteDestination{ |
|
Service: "other", |
|
PrefixRewrite: "/new", |
|
}, |
|
}), |
|
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", |
|
}, |
|
//////////////// |
|
{ |
|
name: "route with no match with retry condition", |
|
entry: makerouter(ServiceRoute{ |
|
Match: nil, |
|
Destination: &ServiceRouteDestination{ |
|
Service: "other", |
|
RetryOn: []string{ |
|
"5xx", |
|
"gateway-error", |
|
"reset", |
|
"connect-failure", |
|
"envoy-ratelimited", |
|
"retriable-4xx", |
|
"refused-stream", |
|
"cancelled", |
|
"deadline-exceeded", |
|
"internal", |
|
"resource-exhausted", |
|
"unavailable", |
|
}, |
|
}, |
|
}), |
|
}, |
|
{ |
|
name: "route with no match with invalid retry condition", |
|
entry: makerouter(ServiceRoute{ |
|
Match: nil, |
|
Destination: &ServiceRouteDestination{ |
|
Service: "other", |
|
RetryOn: []string{ |
|
"invalid-retry-condition", |
|
}, |
|
}, |
|
}), |
|
validateErr: "contains an invalid retry condition: \"invalid-retry-condition\"", |
|
}, |
|
//////////////// |
|
{ |
|
name: "default route with case insensitive match", |
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{ |
|
CaseInsensitive: true, |
|
}))), |
|
}, |
|
{ |
|
name: "route with path prefix and case insensitive match /apI", |
|
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{ |
|
PathPrefix: "/apI", |
|
CaseInsensitive: true, |
|
}))), |
|
}, |
|
} |
|
|
|
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)) |
|
}) |
|
} |
|
} |
|
|
|
func TestIsProtocolHTTPLike(t *testing.T) { |
|
assert.False(t, IsProtocolHTTPLike("")) |
|
assert.False(t, IsProtocolHTTPLike("tcp")) |
|
|
|
assert.True(t, IsProtocolHTTPLike("http")) |
|
assert.True(t, IsProtocolHTTPLike("http2")) |
|
assert.True(t, IsProtocolHTTPLike("grpc")) |
|
} |
|
|
|
func TestIsValidRetryCondition(t *testing.T) { |
|
assert.False(t, isValidRetryCondition("")) |
|
assert.False(t, isValidRetryCondition("retriable-headers")) |
|
assert.False(t, isValidRetryCondition("retriable-status-codes")) |
|
|
|
assert.True(t, isValidRetryCondition("5xx")) |
|
assert.True(t, isValidRetryCondition("gateway-error")) |
|
assert.True(t, isValidRetryCondition("reset")) |
|
assert.True(t, isValidRetryCondition("connect-failure")) |
|
assert.True(t, isValidRetryCondition("envoy-ratelimited")) |
|
assert.True(t, isValidRetryCondition("retriable-4xx")) |
|
assert.True(t, isValidRetryCondition("refused-stream")) |
|
assert.True(t, isValidRetryCondition("cancelled")) |
|
assert.True(t, isValidRetryCondition("deadline-exceeded")) |
|
assert.True(t, isValidRetryCondition("internal")) |
|
assert.True(t, isValidRetryCondition("resource-exhausted")) |
|
assert.True(t, isValidRetryCondition("unavailable")) |
|
}
|
|
|