2017-03-09 16:27:31 +01:00
package healthcheck
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"
2019-08-03 03:58:23 +02:00
"github.com/containous/traefik/v2/pkg/config/runtime"
"github.com/containous/traefik/v2/pkg/testhelpers"
2018-04-16 11:40:03 +02:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2017-03-09 16:27:31 +01:00
"github.com/vulcand/oxy/roundrobin"
)
2018-09-27 13:16:03 -05:00
const healthCheckInterval = 200 * time . Millisecond
const healthCheckTimeout = 100 * time . Millisecond
2017-03-09 16:27:31 +01:00
type testHandler struct {
done func ( )
2018-05-22 09:22:03 +02:00
healthSequence [ ] int
2017-03-09 16:27:31 +01:00
}
func TestSetBackendsConfiguration ( t * testing . T ) {
2018-04-16 11:40:03 +02:00
testCases := [ ] struct {
desc string
startHealthy bool
2018-05-22 09:22:03 +02:00
healthSequence [ ] int
2018-04-16 11:40:03 +02:00
expectedNumRemovedServers int
expectedNumUpsertedServers int
expectedGaugeValue float64
2017-03-09 16:27:31 +01:00
} {
{
2018-04-16 11:40:03 +02:00
desc : "healthy server staying healthy" ,
startHealthy : true ,
2018-05-22 09:22:03 +02:00
healthSequence : [ ] int { http . StatusOK } ,
expectedNumRemovedServers : 0 ,
expectedNumUpsertedServers : 0 ,
expectedGaugeValue : 1 ,
} ,
{
desc : "healthy server staying healthy (StatusNoContent)" ,
startHealthy : true ,
healthSequence : [ ] int { http . StatusNoContent } ,
2018-04-16 11:40:03 +02:00
expectedNumRemovedServers : 0 ,
expectedNumUpsertedServers : 0 ,
expectedGaugeValue : 1 ,
2017-03-09 16:27:31 +01:00
} ,
2018-05-23 17:06:04 +02:00
{
desc : "healthy server staying healthy (StatusPermanentRedirect)" ,
startHealthy : true ,
healthSequence : [ ] int { http . StatusPermanentRedirect } ,
expectedNumRemovedServers : 0 ,
expectedNumUpsertedServers : 0 ,
expectedGaugeValue : 1 ,
} ,
2017-03-09 16:27:31 +01:00
{
2018-04-16 11:40:03 +02:00
desc : "healthy server becoming sick" ,
startHealthy : true ,
2018-05-22 09:22:03 +02:00
healthSequence : [ ] int { http . StatusServiceUnavailable } ,
2018-04-16 11:40:03 +02:00
expectedNumRemovedServers : 1 ,
expectedNumUpsertedServers : 0 ,
expectedGaugeValue : 0 ,
2017-03-09 16:27:31 +01:00
} ,
{
2018-04-16 11:40:03 +02:00
desc : "sick server becoming healthy" ,
startHealthy : false ,
2018-05-22 09:22:03 +02:00
healthSequence : [ ] int { http . StatusOK } ,
2018-04-16 11:40:03 +02:00
expectedNumRemovedServers : 0 ,
expectedNumUpsertedServers : 1 ,
expectedGaugeValue : 1 ,
2017-03-09 16:27:31 +01:00
} ,
{
2018-04-16 11:40:03 +02:00
desc : "sick server staying sick" ,
startHealthy : false ,
2018-05-22 09:22:03 +02:00
healthSequence : [ ] int { http . StatusServiceUnavailable } ,
2018-04-16 11:40:03 +02:00
expectedNumRemovedServers : 0 ,
expectedNumUpsertedServers : 0 ,
expectedGaugeValue : 0 ,
2017-03-09 16:27:31 +01:00
} ,
{
2018-04-16 11:40:03 +02:00
desc : "healthy server toggling to sick and back to healthy" ,
startHealthy : true ,
2018-05-22 09:22:03 +02:00
healthSequence : [ ] int { http . StatusServiceUnavailable , http . StatusOK } ,
2018-04-16 11:40:03 +02:00
expectedNumRemovedServers : 1 ,
expectedNumUpsertedServers : 1 ,
expectedGaugeValue : 1 ,
2017-03-09 16:27:31 +01:00
} ,
}
2018-04-16 11:40:03 +02:00
for _ , test := range testCases {
2017-03-09 16:27:31 +01:00
test := test
t . Run ( test . desc , func ( t * testing . T ) {
t . Parallel ( )
2018-05-22 09:22:03 +02:00
2018-08-06 20:00:03 +02:00
// The context is passed to the health check and canonically canceled by
2017-03-09 16:27:31 +01:00
// the test server once all expected requests have been received.
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
ts := newTestServer ( cancel , test . healthSequence )
defer ts . Close ( )
lb := & testLoadBalancer { RWMutex : & sync . RWMutex { } }
2018-06-11 11:36:03 +02:00
backend := NewBackendConfig ( Options {
2017-03-15 19:16:06 +01:00
Path : "/path" ,
Interval : healthCheckInterval ,
2018-09-27 13:16:03 -05:00
Timeout : healthCheckTimeout ,
2017-03-15 19:16:06 +01:00
LB : lb ,
2018-01-03 12:32:03 +01:00
} , "backendName" )
2018-04-16 11:40:03 +02:00
2017-06-03 15:02:28 +02:00
serverURL := testhelpers . MustParseURL ( ts . URL )
2017-03-09 16:27:31 +01:00
if test . startHealthy {
lb . servers = append ( lb . servers , serverURL )
} else {
2019-08-07 08:14:04 -07:00
backend . disabledURLs = append ( backend . disabledURLs , backendURL { url : serverURL , weight : 1 } )
2017-03-09 16:27:31 +01:00
}
2018-01-26 11:58:03 +01:00
collectingMetrics := testhelpers . NewCollectingHealthCheckMetrics ( )
2017-06-03 15:02:28 +02:00
check := HealthCheck {
2018-06-11 11:36:03 +02:00
Backends : make ( map [ string ] * BackendConfig ) ,
2018-01-26 11:58:03 +01:00
metrics : collectingMetrics ,
2017-03-09 16:27:31 +01:00
}
2018-04-16 11:40:03 +02:00
2017-03-09 16:27:31 +01:00
wg := sync . WaitGroup { }
wg . Add ( 1 )
2018-04-16 11:40:03 +02:00
2017-03-09 16:27:31 +01:00
go func ( ) {
2018-01-03 12:32:03 +01:00
check . execute ( ctx , backend )
2017-03-09 16:27:31 +01:00
wg . Done ( )
} ( )
// Make test timeout dependent on number of expected requests, health
// check interval, and a safety margin.
timeout := time . Duration ( len ( test . healthSequence ) * int ( healthCheckInterval ) + 500 )
select {
case <- time . After ( timeout ) :
t . Fatal ( "test did not complete in time" )
case <- ctx . Done ( ) :
wg . Wait ( )
}
lb . Lock ( )
defer lb . Unlock ( )
2018-01-26 11:58:03 +01:00
2018-04-16 11:40:03 +02:00
assert . Equal ( t , test . expectedNumRemovedServers , lb . numRemovedServers , "removed servers" )
assert . Equal ( t , test . expectedNumUpsertedServers , lb . numUpsertedServers , "upserted servers" )
2018-11-14 10:18:03 +01:00
// FIXME re add metrics
//assert.Equal(t, test.expectedGaugeValue, collectingMetrics.Gauge.GaugeValue, "ServerUp Gauge")
2017-03-09 16:27:31 +01:00
} )
}
}
2017-05-10 14:28:57 -04:00
func TestNewRequest ( t * testing . T ) {
2018-11-15 15:50:03 +01:00
type expected struct {
err bool
value string
}
2018-04-16 11:40:03 +02:00
testCases := [ ] struct {
2018-05-14 12:08:03 +02:00
desc string
serverURL string
options Options
2018-11-15 15:50:03 +01:00
expected expected
2017-05-10 14:28:57 -04:00
} {
{
2018-05-14 12:08:03 +02:00
desc : "no port override" ,
serverURL : "http://backend1:80" ,
options : Options {
Path : "/test" ,
Port : 0 ,
} ,
2018-11-15 15:50:03 +01:00
expected : expected {
err : false ,
value : "http://backend1:80/test" ,
} ,
2017-05-10 14:28:57 -04:00
} ,
{
2018-05-14 12:08:03 +02:00
desc : "port override" ,
serverURL : "http://backend2:80" ,
options : Options {
Path : "/test" ,
Port : 8080 ,
} ,
2018-11-15 15:50:03 +01:00
expected : expected {
err : false ,
value : "http://backend2:8080/test" ,
} ,
2017-05-10 14:28:57 -04:00
} ,
{
2018-05-14 12:08:03 +02:00
desc : "no port override with no port in server URL" ,
serverURL : "http://backend1" ,
options : Options {
Path : "/health" ,
Port : 0 ,
} ,
2018-11-15 15:50:03 +01:00
expected : expected {
err : false ,
value : "http://backend1/health" ,
} ,
2017-05-10 14:28:57 -04:00
} ,
{
2018-05-14 12:08:03 +02:00
desc : "port override with no port in server URL" ,
serverURL : "http://backend2" ,
options : Options {
Path : "/health" ,
Port : 8080 ,
} ,
2018-11-15 15:50:03 +01:00
expected : expected {
err : false ,
value : "http://backend2:8080/health" ,
} ,
2017-05-10 14:28:57 -04:00
} ,
2018-05-14 12:08:03 +02:00
{
desc : "scheme override" ,
serverURL : "https://backend1:80" ,
options : Options {
Scheme : "http" ,
Path : "/test" ,
Port : 0 ,
} ,
2018-11-15 15:50:03 +01:00
expected : expected {
err : false ,
value : "http://backend1:80/test" ,
} ,
} ,
{
desc : "path with param" ,
serverURL : "http://backend1:80" ,
options : Options {
Path : "/health?powpow=do" ,
Port : 0 ,
} ,
expected : expected {
err : false ,
value : "http://backend1:80/health?powpow=do" ,
} ,
} ,
{
desc : "path with params" ,
serverURL : "http://backend1:80" ,
options : Options {
Path : "/health?powpow=do&do=powpow" ,
Port : 0 ,
} ,
expected : expected {
err : false ,
value : "http://backend1:80/health?powpow=do&do=powpow" ,
} ,
} ,
{
desc : "path with invalid path" ,
serverURL : "http://backend1:80" ,
options : Options {
Path : ":" ,
Port : 0 ,
} ,
expected : expected {
err : true ,
value : "" ,
} ,
2018-05-14 12:08:03 +02:00
} ,
2017-05-10 14:28:57 -04:00
}
2018-04-16 11:40:03 +02:00
for _ , test := range testCases {
2017-05-10 14:28:57 -04:00
test := test
t . Run ( test . desc , func ( t * testing . T ) {
t . Parallel ( )
2018-04-16 11:40:03 +02:00
2018-06-11 11:36:03 +02:00
backend := NewBackendConfig ( test . options , "backendName" )
2017-05-10 14:28:57 -04:00
2018-11-15 15:50:03 +01:00
u := testhelpers . MustParseURL ( test . serverURL )
2017-05-10 14:28:57 -04:00
2018-04-16 11:40:03 +02:00
req , err := backend . newRequest ( u )
2018-11-15 15:50:03 +01:00
if test . expected . err {
require . Error ( t , err )
assert . Nil ( t , nil )
} else {
require . NoError ( t , err , "failed to create new backend request" )
require . NotNil ( t , req )
assert . Equal ( t , test . expected . value , req . URL . String ( ) )
}
2018-04-16 11:40:03 +02:00
} )
}
}
2018-05-14 12:08:03 +02:00
func TestAddHeadersAndHost ( t * testing . T ) {
2018-04-16 11:40:03 +02:00
testCases := [ ] struct {
desc string
2018-05-14 12:08:03 +02:00
serverURL string
options Options
2018-04-16 11:40:03 +02:00
expectedHostname string
expectedHeader string
} {
{
2018-05-14 12:08:03 +02:00
desc : "override hostname" ,
serverURL : "http://backend1:80" ,
options : Options {
Hostname : "myhost" ,
Path : "/" ,
} ,
2018-04-16 11:40:03 +02:00
expectedHostname : "myhost" ,
expectedHeader : "" ,
} ,
{
2018-05-14 12:08:03 +02:00
desc : "not override hostname" ,
serverURL : "http://backend1:80" ,
options : Options {
Hostname : "" ,
Path : "/" ,
} ,
2018-04-16 11:40:03 +02:00
expectedHostname : "backend1:80" ,
expectedHeader : "" ,
} ,
{
2018-05-14 12:08:03 +02:00
desc : "custom header" ,
serverURL : "http://backend1:80" ,
options : Options {
Headers : map [ string ] string { "Custom-Header" : "foo" } ,
Hostname : "" ,
Path : "/" ,
} ,
2018-04-16 11:40:03 +02:00
expectedHostname : "backend1:80" ,
expectedHeader : "foo" ,
} ,
{
2018-05-14 12:08:03 +02:00
desc : "custom header with hostname override" ,
serverURL : "http://backend1:80" ,
options : Options {
Headers : map [ string ] string { "Custom-Header" : "foo" } ,
Hostname : "myhost" ,
Path : "/" ,
} ,
2018-04-16 11:40:03 +02:00
expectedHostname : "myhost" ,
expectedHeader : "foo" ,
} ,
}
for _ , test := range testCases {
test := test
t . Run ( test . desc , func ( t * testing . T ) {
t . Parallel ( )
2018-06-11 11:36:03 +02:00
backend := NewBackendConfig ( test . options , "backendName" )
2018-04-16 11:40:03 +02:00
2018-05-14 12:08:03 +02:00
u , err := url . Parse ( test . serverURL )
require . NoError ( t , err )
2018-04-16 11:40:03 +02:00
2017-05-10 14:28:57 -04:00
req , err := backend . newRequest ( u )
2018-05-14 12:08:03 +02:00
require . NoError ( t , err , "failed to create new backend request" )
2017-05-10 14:28:57 -04:00
2018-04-16 11:40:03 +02:00
req = backend . addHeadersAndHost ( req )
2018-05-14 12:08:03 +02:00
assert . Equal ( t , "http://backend1:80/" , req . URL . String ( ) )
2018-04-16 11:40:03 +02:00
assert . Equal ( t , test . expectedHostname , req . Host )
assert . Equal ( t , test . expectedHeader , req . Header . Get ( "Custom-Header" ) )
2017-05-10 14:28:57 -04:00
} )
}
2017-06-03 15:02:28 +02:00
}
type testLoadBalancer struct {
// RWMutex needed due to parallel test execution: Both the system-under-test
// and the test assertions reference the counters.
* sync . RWMutex
numRemovedServers int
numUpsertedServers int
servers [ ] * url . URL
2019-05-16 10:58:06 +02:00
// options is just to make sure that LBStatusUpdater forwards options on Upsert to its BalancerHandler
options [ ] roundrobin . ServerOption
2017-06-03 15:02:28 +02:00
}
2017-05-10 14:28:57 -04:00
2018-06-11 11:36:03 +02:00
func ( lb * testLoadBalancer ) ServeHTTP ( w http . ResponseWriter , req * http . Request ) {
// noop
}
2017-06-03 15:02:28 +02:00
func ( lb * testLoadBalancer ) RemoveServer ( u * url . URL ) error {
lb . Lock ( )
defer lb . Unlock ( )
lb . numRemovedServers ++
lb . removeServer ( u )
return nil
2017-05-10 14:28:57 -04:00
}
2017-06-03 15:02:28 +02:00
func ( lb * testLoadBalancer ) UpsertServer ( u * url . URL , options ... roundrobin . ServerOption ) error {
lb . Lock ( )
defer lb . Unlock ( )
lb . numUpsertedServers ++
lb . servers = append ( lb . servers , u )
2019-05-16 10:58:06 +02:00
lb . options = append ( lb . options , options ... )
2017-06-03 15:02:28 +02:00
return nil
}
func ( lb * testLoadBalancer ) Servers ( ) [ ] * url . URL {
return lb . servers
}
2019-05-16 10:58:06 +02:00
func ( lb * testLoadBalancer ) Options ( ) [ ] roundrobin . ServerOption {
return lb . options
}
2017-06-03 15:02:28 +02:00
func ( lb * testLoadBalancer ) removeServer ( u * url . URL ) {
var i int
var serverURL * url . URL
2019-05-16 10:58:06 +02:00
found := false
2017-06-03 15:02:28 +02:00
for i , serverURL = range lb . servers {
if * serverURL == * u {
2019-05-16 10:58:06 +02:00
found = true
2017-06-03 15:02:28 +02:00
break
}
}
2019-05-16 10:58:06 +02:00
if ! found {
return
}
2017-06-03 15:02:28 +02:00
lb . servers = append ( lb . servers [ : i ] , lb . servers [ i + 1 : ] ... )
}
2018-05-22 09:22:03 +02:00
func newTestServer ( done func ( ) , healthSequence [ ] int ) * httptest . Server {
2017-06-03 15:02:28 +02:00
handler := & testHandler {
done : done ,
healthSequence : healthSequence ,
}
return httptest . NewServer ( handler )
}
2018-05-22 09:22:03 +02:00
// ServeHTTP returns HTTP response codes following a status sequences.
// It calls the given 'done' function once all request health indicators have been depleted.
2017-06-03 15:02:28 +02:00
func ( th * testHandler ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
if len ( th . healthSequence ) == 0 {
panic ( "received unexpected request" )
}
2018-05-22 09:22:03 +02:00
w . WriteHeader ( th . healthSequence [ 0 ] )
2017-06-03 15:02:28 +02:00
th . healthSequence = th . healthSequence [ 1 : ]
if len ( th . healthSequence ) == 0 {
th . done ( )
2017-03-09 16:27:31 +01:00
}
}
2019-05-16 10:58:06 +02:00
func TestLBStatusUpdater ( t * testing . T ) {
lb := & testLoadBalancer { RWMutex : & sync . RWMutex { } }
2019-07-15 17:04:04 +02:00
svInfo := & runtime . ServiceInfo { }
2019-05-16 10:58:06 +02:00
lbsu := NewLBStatusUpdater ( lb , svInfo )
newServer , err := url . Parse ( "http://foo.com" )
assert . Nil ( t , err )
err = lbsu . UpsertServer ( newServer , roundrobin . Weight ( 1 ) )
assert . Nil ( t , err )
assert . Equal ( t , len ( lbsu . Servers ( ) ) , 1 )
assert . Equal ( t , len ( lbsu . BalancerHandler . ( * testLoadBalancer ) . Options ( ) ) , 1 )
statuses := svInfo . GetAllStatus ( )
assert . Equal ( t , len ( statuses ) , 1 )
for k , v := range statuses {
assert . Equal ( t , k , newServer . String ( ) )
assert . Equal ( t , v , serverUp )
break
}
err = lbsu . RemoveServer ( newServer )
assert . Nil ( t , err )
assert . Equal ( t , len ( lbsu . Servers ( ) ) , 0 )
statuses = svInfo . GetAllStatus ( )
assert . Equal ( t , len ( statuses ) , 1 )
for k , v := range statuses {
assert . Equal ( t , k , newServer . String ( ) )
assert . Equal ( t , v , serverDown )
break
}
}