respond with 503 on empty backend
This commit is contained in:
parent
16609cd485
commit
074b31b5e9
5 changed files with 275 additions and 3 deletions
|
@ -59,8 +59,8 @@ func (s *HealthCheckSuite) TestSimpleConfiguration(c *check.C) {
|
||||||
// Waiting for Traefik healthcheck
|
// Waiting for Traefik healthcheck
|
||||||
try.Sleep(2 * time.Second)
|
try.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// Verify frontend health : 500
|
// Verify no backend service is available due to failing health checks
|
||||||
err = try.Request(frontendHealthReq, 3*time.Second, try.StatusCodeIs(http.StatusInternalServerError))
|
err = try.Request(frontendHealthReq, 3*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Change one whoami health to 200
|
// Change one whoami health to 200
|
||||||
|
@ -77,7 +77,7 @@ func (s *HealthCheckSuite) TestSimpleConfiguration(c *check.C) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
frontendReq.Host = "test.localhost"
|
frontendReq.Host = "test.localhost"
|
||||||
|
|
||||||
// Check if whoami1 respond
|
// Check if whoami1 responds
|
||||||
err = try.Request(frontendReq, 500*time.Millisecond, try.BodyContains(whoami1Host))
|
err = try.Request(frontendReq, 500*time.Millisecond, try.BodyContains(whoami1Host))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
|
31
middlewares/empty_backend_handler.go
Normal file
31
middlewares/empty_backend_handler.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/healthcheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmptyBackendHandler is a middlware that checks whether the current Backend
|
||||||
|
// has at least one active Server in respect to the healthchecks and if this
|
||||||
|
// is not the case, it will stop the middleware chain and respond with 503.
|
||||||
|
type EmptyBackendHandler struct {
|
||||||
|
lb healthcheck.LoadBalancer
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEmptyBackendHandler creates a new EmptyBackendHandler instance.
|
||||||
|
func NewEmptyBackendHandler(lb healthcheck.LoadBalancer, next http.Handler) *EmptyBackendHandler {
|
||||||
|
return &EmptyBackendHandler{lb: lb, next: next}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP responds with 503 when there is no active Server and otherwise
|
||||||
|
// invokes the next handler in the middleware chain.
|
||||||
|
func (h *EmptyBackendHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if len(h.lb.Servers()) == 0 {
|
||||||
|
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
rw.Write([]byte(http.StatusText(http.StatusServiceUnavailable)))
|
||||||
|
} else {
|
||||||
|
h.next.ServeHTTP(rw, r)
|
||||||
|
}
|
||||||
|
}
|
70
middlewares/empty_backend_handler_test.go
Normal file
70
middlewares/empty_backend_handler_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/testhelpers"
|
||||||
|
"github.com/vulcand/oxy/roundrobin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmptyBackendHandler(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
amountServer int
|
||||||
|
wantStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
amountServer: 0,
|
||||||
|
wantStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
amountServer: 1,
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("amount servers %d", test.amountServer), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
handler := NewEmptyBackendHandler(&healthCheckLoadBalancer{test.amountServer}, nextHandler)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
if recorder.Result().StatusCode != test.wantStatusCode {
|
||||||
|
t.Errorf("Received status code %d, wanted %d", recorder.Result().StatusCode, test.wantStatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type healthCheckLoadBalancer struct {
|
||||||
|
amountServer int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lb *healthCheckLoadBalancer) RemoveServer(u *url.URL) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lb *healthCheckLoadBalancer) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lb *healthCheckLoadBalancer) Servers() []*url.URL {
|
||||||
|
servers := make([]*url.URL, lb.amountServer)
|
||||||
|
for i := 0; i < lb.amountServer; i++ {
|
||||||
|
servers = append(servers, testhelpers.MustParseURL("http://localhost"))
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
|
@ -744,6 +744,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||||
log.Debugf("Setting up backend health check %s", *hcOpts)
|
log.Debugf("Setting up backend health check %s", *hcOpts)
|
||||||
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
|
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
|
||||||
}
|
}
|
||||||
|
lb = middlewares.NewEmptyBackendHandler(rebalancer, lb)
|
||||||
case types.Wrr:
|
case types.Wrr:
|
||||||
log.Debugf("Creating load-balancer wrr")
|
log.Debugf("Creating load-balancer wrr")
|
||||||
if stickysession {
|
if stickysession {
|
||||||
|
@ -764,6 +765,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||||
log.Debugf("Setting up backend health check %s", *hcOpts)
|
log.Debugf("Setting up backend health check %s", *hcOpts)
|
||||||
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
|
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
|
||||||
}
|
}
|
||||||
|
lb = middlewares.NewEmptyBackendHandler(rr, lb)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(frontend.Errors) > 0 {
|
if len(frontend.Errors) > 0 {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -576,3 +577,171 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerResponseEmptyBackend(t *testing.T) {
|
||||||
|
const requestPath = "/path"
|
||||||
|
const routeRule = "Path:" + requestPath
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
dynamicConfig func(testServerURL string) *types.Configuration
|
||||||
|
wantStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Ok",
|
||||||
|
dynamicConfig: func(testServerURL string) *types.Configuration {
|
||||||
|
return buildDynamicConfig(
|
||||||
|
withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))),
|
||||||
|
withBackend("backend", buildBackend(withServer("testServer", testServerURL))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "No Frontend",
|
||||||
|
dynamicConfig: func(testServerURL string) *types.Configuration {
|
||||||
|
return buildDynamicConfig()
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB-Drr",
|
||||||
|
dynamicConfig: func(testServerURL string) *types.Configuration {
|
||||||
|
return buildDynamicConfig(
|
||||||
|
withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))),
|
||||||
|
withBackend("backend", buildBackend(withLoadBalancer("Drr", false))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB-Drr Sticky",
|
||||||
|
dynamicConfig: func(testServerURL string) *types.Configuration {
|
||||||
|
return buildDynamicConfig(
|
||||||
|
withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))),
|
||||||
|
withBackend("backend", buildBackend(withLoadBalancer("Drr", true))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB-Wrr",
|
||||||
|
dynamicConfig: func(testServerURL string) *types.Configuration {
|
||||||
|
return buildDynamicConfig(
|
||||||
|
withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))),
|
||||||
|
withBackend("backend", buildBackend(withLoadBalancer("Wrr", false))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB-Wrr Sticky",
|
||||||
|
dynamicConfig: func(testServerURL string) *types.Configuration {
|
||||||
|
return buildDynamicConfig(
|
||||||
|
withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))),
|
||||||
|
withBackend("backend", buildBackend(withLoadBalancer("Wrr", true))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
globalConfig := GlobalConfiguration{
|
||||||
|
EntryPoints: EntryPoints{
|
||||||
|
"http": &EntryPoint{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dynamicConfigs := configs{"config": test.dynamicConfig(testServer.URL)}
|
||||||
|
|
||||||
|
srv := NewServer(globalConfig)
|
||||||
|
entryPoints, err := srv.loadConfig(dynamicConfigs, globalConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error loading config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseRecorder := &httptest.ResponseRecorder{}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil)
|
||||||
|
|
||||||
|
entryPoints["http"].httpRouter.ServeHTTP(responseRecorder, request)
|
||||||
|
|
||||||
|
if responseRecorder.Result().StatusCode != test.wantStatusCode {
|
||||||
|
t.Errorf("got status code %d, want %d", responseRecorder.Result().StatusCode, test.wantStatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDynamicConfig(dynamicConfigBuilders ...func(*types.Configuration)) *types.Configuration {
|
||||||
|
config := &types.Configuration{
|
||||||
|
Frontends: make(map[string]*types.Frontend),
|
||||||
|
Backends: make(map[string]*types.Backend),
|
||||||
|
}
|
||||||
|
for _, build := range dynamicConfigBuilders {
|
||||||
|
build(config)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func withFrontend(frontendName string, frontend *types.Frontend) func(*types.Configuration) {
|
||||||
|
return func(config *types.Configuration) {
|
||||||
|
config.Frontends[frontendName] = frontend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withBackend(backendName string, backend *types.Backend) func(*types.Configuration) {
|
||||||
|
return func(config *types.Configuration) {
|
||||||
|
config.Backends[backendName] = backend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFrontend(frontendBuilders ...func(*types.Frontend)) *types.Frontend {
|
||||||
|
fe := &types.Frontend{
|
||||||
|
EntryPoints: []string{"http"},
|
||||||
|
Backend: "backend",
|
||||||
|
Routes: make(map[string]types.Route),
|
||||||
|
}
|
||||||
|
for _, build := range frontendBuilders {
|
||||||
|
build(fe)
|
||||||
|
}
|
||||||
|
return fe
|
||||||
|
}
|
||||||
|
|
||||||
|
func withRoute(routeName, rule string) func(*types.Frontend) {
|
||||||
|
return func(fe *types.Frontend) {
|
||||||
|
fe.Routes[routeName] = types.Route{Rule: rule}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBackend(backendBuilders ...func(*types.Backend)) *types.Backend {
|
||||||
|
be := &types.Backend{
|
||||||
|
Servers: make(map[string]types.Server),
|
||||||
|
LoadBalancer: &types.LoadBalancer{Method: "Wrr"},
|
||||||
|
}
|
||||||
|
for _, build := range backendBuilders {
|
||||||
|
build(be)
|
||||||
|
}
|
||||||
|
return be
|
||||||
|
}
|
||||||
|
|
||||||
|
func withServer(name, url string) func(backend *types.Backend) {
|
||||||
|
return func(be *types.Backend) {
|
||||||
|
be.Servers[name] = types.Server{URL: url}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withLoadBalancer(method string, sticky bool) func(*types.Backend) {
|
||||||
|
return func(be *types.Backend) {
|
||||||
|
be.LoadBalancer = &types.LoadBalancer{Method: method, Sticky: sticky}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue