// 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" ) )
}