// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package topoutil
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testing/deployer/topology"
)
// CheckBlankspaceNameViaHTTP calls a copy of blankspace and asserts it arrived
// on the correct instance using HTTP1 or HTTP2.
func ( a * Asserter ) CheckBlankspaceNameViaHTTP (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
useHTTP2 bool ,
path string ,
clusterName string ,
sid topology . ID ,
) {
t . Helper ( )
a . checkBlankspaceNameViaHTTPWithCallback ( t , workload , us , useHTTP2 , path , 1 , func ( _ * retry . R ) { } , func ( r * retry . R , remoteName string ) {
require . Equal ( r , fmt . Sprintf ( "%s::%s" , clusterName , sid . String ( ) ) , remoteName )
} , func ( r * retry . R ) { } )
}
// CheckBlankspaceNameTrafficSplitViaHTTP is like CheckBlankspaceNameViaHTTP
// but it is verifying a relative traffic split.
func ( a * Asserter ) CheckBlankspaceNameTrafficSplitViaHTTP (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
useHTTP2 bool ,
path string ,
expect map [ string ] int ,
) {
t . Helper ( )
got := make ( map [ string ] int )
a . checkBlankspaceNameViaHTTPWithCallback ( t , workload , us , useHTTP2 , path , 100 , func ( _ * retry . R ) {
got = make ( map [ string ] int )
} , func ( _ * retry . R , name string ) {
got [ name ] ++
} , func ( r * retry . R ) {
assertTrafficSplitFor100Requests ( r , got , expect )
} )
}
func ( a * Asserter ) checkBlankspaceNameViaHTTPWithCallback (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
useHTTP2 bool ,
path string ,
count int ,
resetFn func ( r * retry . R ) ,
attemptFn func ( r * retry . R , remoteName string ) ,
checkFn func ( r * retry . R ) ,
) {
t . Helper ( )
var (
node = workload . Node
internalPort = workload . Port
addr = fmt . Sprintf ( "%s:%d" , node . LocalAddress ( ) , internalPort )
client = a . mustGetHTTPClient ( t , node . Cluster )
)
if useHTTP2 {
// We can't use the forward proxy for http2, so use the exposed port on localhost instead.
exposedPort := node . ExposedPort ( internalPort )
require . True ( t , exposedPort > 0 )
addr = fmt . Sprintf ( "%s:%d" , "127.0.0.1" , exposedPort )
// This will clear the proxy field on the transport.
client = EnableHTTP2 ( client )
}
actualURL := fmt . Sprintf ( "http://localhost:%d/%s" , us . LocalPort , path )
multiassert ( t , count , resetFn , func ( r * retry . R ) {
name , err := GetBlankspaceNameViaHTTP ( context . Background ( ) , client , addr , actualURL )
require . NoError ( r , err )
attemptFn ( r , name )
} , func ( r * retry . R ) {
checkFn ( r )
} )
}
// CheckBlankspaceNameViaTCP calls a copy of blankspace and asserts it arrived
// on the correct instance using plain tcp sockets.
func ( a * Asserter ) CheckBlankspaceNameViaTCP (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
clusterName string ,
sid topology . ID ,
) {
t . Helper ( )
a . checkBlankspaceNameViaTCPWithCallback ( t , workload , us , 1 , func ( _ * retry . R ) { } , func ( r * retry . R , remoteName string ) {
require . Equal ( r , fmt . Sprintf ( "%s::%s" , clusterName , sid . String ( ) ) , remoteName )
} , func ( r * retry . R ) { } )
}
// CheckBlankspaceNameTrafficSplitViaTCP is like CheckBlankspaceNameViaTCP
// but it is verifying a relative traffic split.
func ( a * Asserter ) CheckBlankspaceNameTrafficSplitViaTCP (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
expect map [ string ] int ,
) {
t . Helper ( )
got := make ( map [ string ] int )
a . checkBlankspaceNameViaTCPWithCallback ( t , workload , us , 100 , func ( _ * retry . R ) {
got = make ( map [ string ] int )
} , func ( _ * retry . R , name string ) {
got [ name ] ++
} , func ( r * retry . R ) {
assertTrafficSplitFor100Requests ( r , got , expect )
} )
}
func ( a * Asserter ) checkBlankspaceNameViaTCPWithCallback (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
count int ,
resetFn func ( r * retry . R ) ,
attemptFn func ( r * retry . R , remoteName string ) ,
checkFn func ( r * retry . R ) ,
) {
t . Helper ( )
port := us . LocalPort
require . True ( t , port > 0 )
node := workload . Node
// We can't use the forward proxy for TCP yet, so use the exposed port on localhost instead.
exposedPort := node . ExposedPort ( port )
require . True ( t , exposedPort > 0 )
addr := fmt . Sprintf ( "%s:%d" , "127.0.0.1" , exposedPort )
multiassert ( t , count , resetFn , func ( r * retry . R ) {
name , err := GetBlankspaceNameViaTCP ( context . Background ( ) , addr )
require . NoError ( r , err )
attemptFn ( r , name )
} , func ( r * retry . R ) {
checkFn ( r )
} )
}
// CheckBlankspaceNameViaGRPC calls a copy of blankspace and asserts it arrived
// on the correct instance using gRPC.
func ( a * Asserter ) CheckBlankspaceNameViaGRPC (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
clusterName string ,
sid topology . ID ,
) {
t . Helper ( )
a . checkBlankspaceNameViaGRPCWithCallback ( t , workload , us , 1 , func ( _ * retry . R ) { } , func ( r * retry . R , remoteName string ) {
require . Equal ( r , fmt . Sprintf ( "%s::%s" , clusterName , sid . String ( ) ) , remoteName )
} , func ( _ * retry . R ) { } )
}
// CheckBlankspaceNameTrafficSplitViaGRPC is like CheckBlankspaceNameViaGRPC
// but it is verifying a relative traffic split.
func ( a * Asserter ) CheckBlankspaceNameTrafficSplitViaGRPC (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
expect map [ string ] int ,
) {
t . Helper ( )
got := make ( map [ string ] int )
a . checkBlankspaceNameViaGRPCWithCallback ( t , workload , us , 100 , func ( _ * retry . R ) {
got = make ( map [ string ] int )
} , func ( _ * retry . R , name string ) {
got [ name ] ++
} , func ( r * retry . R ) {
assertTrafficSplitFor100Requests ( r , got , expect )
} )
}
func ( a * Asserter ) checkBlankspaceNameViaGRPCWithCallback (
t * testing . T ,
workload * topology . Workload ,
us * topology . Upstream ,
count int ,
resetFn func ( r * retry . R ) ,
attemptFn func ( r * retry . R , remoteName string ) ,
checkFn func ( r * retry . R ) ,
) {
t . Helper ( )
port := us . LocalPort
require . True ( t , port > 0 )
node := workload . Node
// We can't use the forward proxy for gRPC yet, so use the exposed port on localhost instead.
exposedPort := node . ExposedPort ( port )
require . True ( t , exposedPort > 0 )
addr := fmt . Sprintf ( "%s:%d" , "127.0.0.1" , exposedPort )
multiassert ( t , count , resetFn , func ( r * retry . R ) {
name , err := GetBlankspaceNameViaGRPC ( context . Background ( ) , addr )
require . NoError ( r , err )
attemptFn ( r , name )
} , func ( r * retry . R ) {
checkFn ( r )
} )
}
// assertTrafficSplitFor100Requests compares the counts of 100 requests that
// did reach an observed set of upstreams (nameCounts) against the expected
// counts of those same services is the same within a fixed difference of 2.
func assertTrafficSplitFor100Requests ( t require . TestingT , nameCounts map [ string ] int , expect map [ string ] int ) {
const (
numRequests = 100
allowedDelta = 2
)
require . Equal ( t , numRequests , sumMapValues ( nameCounts ) , "measured traffic was not %d requests" , numRequests )
require . Equal ( t , numRequests , sumMapValues ( expect ) , "expected traffic was not %d requests" , numRequests )
assertTrafficSplit ( t , nameCounts , expect , allowedDelta )
}
func sumMapValues ( m map [ string ] int ) int {
sum := 0
for _ , v := range m {
sum += v
}
return sum
}
// assertTrafficSplit compares the counts of requests that did reach an
// observed set of upstreams (nameCounts) against the expected counts of
// those same services is the same within the provided allowedDelta value.
//
// When doing random traffic splits it'll never be perfect so we need the
// wiggle room to avoid having a flaky test.
func assertTrafficSplit ( t require . TestingT , nameCounts map [ string ] int , expect map [ string ] int , allowedDelta int ) {
require . Len ( t , nameCounts , len ( expect ) )
for name , expectCount := range expect {
gotCount , ok := nameCounts [ name ]
require . True ( t , ok )
if len ( expect ) == 1 {
require . Equal ( t , expectCount , gotCount )
} else {
require . InDelta ( t , expectCount , gotCount , float64 ( allowedDelta ) ,
"expected %q side of split to have %d requests not %d (e=%d)" ,
name , expectCount , gotCount , allowedDelta ,
)
}
}
}
// multiassert will retry in bulk calling attemptFn count times and following
// that with one last call to checkFn.
//
// It's primary use at the time it was written was to execute a set of requests
// repeatedly to witness where the requests went, and then at the end doing a
// verification of traffic splits (a bit like MAP/REDUCE).
func multiassert ( t * testing . T , count int , resetFn , attemptFn , checkFn func ( r * retry . R ) ) {
retry . RunWith ( & retry . Timer { Timeout : 30 * time . Second , Wait : 500 * time . Millisecond } , t , func ( r * retry . R ) {
resetFn ( r )
for i := 0 ; i < count ; i ++ {
attemptFn ( r )
}
checkFn ( r )
} )
}