2018-11-14 09:18:03 +00:00
package service
import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"time"
2019-01-18 14:18:04 +00:00
"github.com/containous/alice"
2019-07-10 07:26:04 +00:00
"github.com/containous/traefik/pkg/config/dynamic"
2019-07-15 15:04:04 +00:00
"github.com/containous/traefik/pkg/config/runtime"
2019-03-15 08:42:03 +00:00
"github.com/containous/traefik/pkg/healthcheck"
"github.com/containous/traefik/pkg/log"
2019-07-18 19:36:05 +00:00
"github.com/containous/traefik/pkg/metrics"
2019-03-15 08:42:03 +00:00
"github.com/containous/traefik/pkg/middlewares/accesslog"
"github.com/containous/traefik/pkg/middlewares/emptybackendhandler"
2019-07-18 19:36:05 +00:00
metricsMiddle "github.com/containous/traefik/pkg/middlewares/metrics"
2019-03-18 10:30:07 +00:00
"github.com/containous/traefik/pkg/middlewares/pipelining"
2019-03-15 08:42:03 +00:00
"github.com/containous/traefik/pkg/server/cookie"
"github.com/containous/traefik/pkg/server/internal"
2018-11-14 09:18:03 +00:00
"github.com/vulcand/oxy/roundrobin"
)
const (
defaultHealthCheckInterval = 30 * time . Second
defaultHealthCheckTimeout = 5 * time . Second
)
// NewManager creates a new Manager
2019-07-18 19:36:05 +00:00
func NewManager ( configs map [ string ] * runtime . ServiceInfo , defaultRoundTripper http . RoundTripper , metricsRegistry metrics . Registry ) * Manager {
2018-11-14 09:18:03 +00:00
return & Manager {
2019-07-18 19:36:05 +00:00
metricsRegistry : metricsRegistry ,
2018-11-14 09:18:03 +00:00
bufferPool : newBufferPool ( ) ,
defaultRoundTripper : defaultRoundTripper ,
balancers : make ( map [ string ] [ ] healthcheck . BalancerHandler ) ,
configs : configs ,
}
}
// Manager The service manager
type Manager struct {
2019-07-18 19:36:05 +00:00
metricsRegistry metrics . Registry
2018-11-14 09:18:03 +00:00
bufferPool httputil . BufferPool
defaultRoundTripper http . RoundTripper
balancers map [ string ] [ ] healthcheck . BalancerHandler
2019-07-15 15:04:04 +00:00
configs map [ string ] * runtime . ServiceInfo
2018-11-14 09:18:03 +00:00
}
2019-03-14 08:30:04 +00:00
// BuildHTTP Creates a http.Handler for a service configuration.
func ( m * Manager ) BuildHTTP ( rootCtx context . Context , serviceName string , responseModifier func ( * http . Response ) error ) ( http . Handler , error ) {
2018-11-14 09:18:03 +00:00
ctx := log . With ( rootCtx , log . Str ( log . ServiceName , serviceName ) )
2019-01-15 13:28:04 +00:00
serviceName = internal . GetQualifiedName ( ctx , serviceName )
ctx = internal . AddProviderInContext ( ctx , serviceName )
2019-05-16 08:58:06 +00:00
conf , ok := m . configs [ serviceName ]
if ! ok {
return nil , fmt . Errorf ( "the service %q does not exist" , serviceName )
}
// TODO Should handle multiple service types
// FIXME Check if the service is declared multiple times with different types
if conf . LoadBalancer == nil {
2019-07-15 15:04:04 +00:00
sErr := fmt . Errorf ( "the service %q doesn't have any load balancer" , serviceName )
conf . AddError ( sErr , true )
return nil , sErr
2018-11-14 09:18:03 +00:00
}
2019-05-16 08:58:06 +00:00
lb , err := m . getLoadBalancerServiceHandler ( ctx , serviceName , conf . LoadBalancer , responseModifier )
if err != nil {
2019-07-15 15:04:04 +00:00
conf . AddError ( err , true )
2019-05-16 08:58:06 +00:00
return nil , err
}
return lb , nil
2018-11-14 09:18:03 +00:00
}
func ( m * Manager ) getLoadBalancerServiceHandler (
ctx context . Context ,
serviceName string ,
2019-07-10 07:26:04 +00:00
service * dynamic . LoadBalancerService ,
2018-11-14 09:18:03 +00:00
responseModifier func ( * http . Response ) error ,
) ( http . Handler , error ) {
2019-03-14 08:30:04 +00:00
fwd , err := buildProxy ( service . PassHostHeader , service . ResponseForwarding , m . defaultRoundTripper , m . bufferPool , responseModifier )
2018-11-14 09:18:03 +00:00
if err != nil {
return nil , err
}
2019-01-18 14:18:04 +00:00
alHandler := func ( next http . Handler ) ( http . Handler , error ) {
return accesslog . NewFieldHandler ( next , accesslog . ServiceName , serviceName , accesslog . AddServiceFields ) , nil
}
2019-07-18 19:36:05 +00:00
chain := alice . New ( )
if m . metricsRegistry != nil && m . metricsRegistry . IsSvcEnabled ( ) {
chain = chain . Append ( metricsMiddle . WrapServiceHandler ( ctx , m . metricsRegistry , serviceName ) )
}
2018-11-14 09:18:03 +00:00
2019-07-18 19:36:05 +00:00
handler , err := chain . Append ( alHandler ) . Then ( pipelining . New ( ctx , fwd , "pipelining" ) )
2018-11-14 09:18:03 +00:00
if err != nil {
return nil , err
}
2019-01-18 14:18:04 +00:00
balancer , err := m . getLoadBalancer ( ctx , serviceName , service , handler )
2018-11-14 09:18:03 +00:00
if err != nil {
return nil , err
}
// TODO rename and checks
m . balancers [ serviceName ] = append ( m . balancers [ serviceName ] , balancer )
// Empty (backend with no servers)
return emptybackendhandler . New ( balancer ) , nil
}
// LaunchHealthCheck Launches the health checks.
func ( m * Manager ) LaunchHealthCheck ( ) {
backendConfigs := make ( map [ string ] * healthcheck . BackendConfig )
for serviceName , balancers := range m . balancers {
ctx := log . With ( context . Background ( ) , log . Str ( log . ServiceName , serviceName ) )
2019-03-18 10:30:07 +00:00
// TODO aggregate
2018-11-14 09:18:03 +00:00
balancer := balancers [ 0 ]
2019-03-18 10:30:07 +00:00
// TODO Should all the services handle healthcheck? Handle different types
2018-11-14 09:18:03 +00:00
service := m . configs [ serviceName ] . LoadBalancer
// Health Check
var backendHealthCheck * healthcheck . BackendConfig
if hcOpts := buildHealthCheckOptions ( ctx , balancer , serviceName , service . HealthCheck ) ; hcOpts != nil {
log . FromContext ( ctx ) . Debugf ( "Setting up healthcheck for service %s with %s" , serviceName , * hcOpts )
hcOpts . Transport = m . defaultRoundTripper
backendHealthCheck = healthcheck . NewBackendConfig ( * hcOpts , serviceName )
}
if backendHealthCheck != nil {
backendConfigs [ serviceName ] = backendHealthCheck
}
}
// FIXME metrics and context
healthcheck . GetHealthCheck ( ) . SetBackendsConfiguration ( context . TODO ( ) , backendConfigs )
}
2019-07-10 07:26:04 +00:00
func buildHealthCheckOptions ( ctx context . Context , lb healthcheck . BalancerHandler , backend string , hc * dynamic . HealthCheck ) * healthcheck . Options {
2018-11-14 09:18:03 +00:00
if hc == nil || hc . Path == "" {
return nil
}
logger := log . FromContext ( ctx )
interval := defaultHealthCheckInterval
if hc . Interval != "" {
intervalOverride , err := time . ParseDuration ( hc . Interval )
2019-02-05 16:10:03 +00:00
switch {
case err != nil :
2018-11-14 09:18:03 +00:00
logger . Errorf ( "Illegal health check interval for '%s': %s" , backend , err )
2019-02-05 16:10:03 +00:00
case intervalOverride <= 0 :
2018-11-14 09:18:03 +00:00
logger . Errorf ( "Health check interval smaller than zero for service '%s'" , backend )
2019-02-05 16:10:03 +00:00
default :
2018-11-14 09:18:03 +00:00
interval = intervalOverride
}
}
timeout := defaultHealthCheckTimeout
if hc . Timeout != "" {
timeoutOverride , err := time . ParseDuration ( hc . Timeout )
2019-02-05 16:10:03 +00:00
switch {
case err != nil :
2018-11-14 09:18:03 +00:00
logger . Errorf ( "Illegal health check timeout for backend '%s': %s" , backend , err )
2019-02-05 16:10:03 +00:00
case timeoutOverride <= 0 :
2018-11-14 09:18:03 +00:00
logger . Errorf ( "Health check timeout smaller than zero for backend '%s', backend" , backend )
2019-02-05 16:10:03 +00:00
default :
2018-11-14 09:18:03 +00:00
timeout = timeoutOverride
}
}
if timeout >= interval {
2019-05-16 08:58:06 +00:00
logger . Warnf ( "Health check timeout for backend '%s' should be lower than the health check interval. Interval set to timeout + 1 second (%s)." , backend , interval )
2018-11-14 09:18:03 +00:00
}
return & healthcheck . Options {
Scheme : hc . Scheme ,
Path : hc . Path ,
Port : hc . Port ,
Interval : interval ,
Timeout : timeout ,
LB : lb ,
Hostname : hc . Hostname ,
Headers : hc . Headers ,
}
}
2019-07-10 07:26:04 +00:00
func ( m * Manager ) getLoadBalancer ( ctx context . Context , serviceName string , service * dynamic . LoadBalancerService , fwd http . Handler ) ( healthcheck . BalancerHandler , error ) {
2018-11-14 09:18:03 +00:00
logger := log . FromContext ( ctx )
2019-06-05 20:18:06 +00:00
logger . Debug ( "Creating load-balancer" )
var options [ ] roundrobin . LBOption
2018-11-14 09:18:03 +00:00
var cookieName string
if stickiness := service . Stickiness ; stickiness != nil {
cookieName = cookie . GetName ( stickiness . CookieName , serviceName )
2019-06-12 22:42:06 +00:00
opts := roundrobin . CookieOptions { HTTPOnly : stickiness . HTTPOnlyCookie , Secure : stickiness . SecureCookie }
options = append ( options , roundrobin . EnableStickySession ( roundrobin . NewStickySessionWithOptions ( cookieName , opts ) ) )
2019-06-05 20:18:06 +00:00
logger . Debugf ( "Sticky session cookie name: %v" , cookieName )
2018-11-14 09:18:03 +00:00
}
2019-06-05 20:18:06 +00:00
lb , err := roundrobin . New ( fwd , options ... )
if err != nil {
return nil , err
2018-11-14 09:18:03 +00:00
}
2019-05-16 08:58:06 +00:00
lbsu := healthcheck . NewLBStatusUpdater ( lb , m . configs [ serviceName ] )
if err := m . upsertServers ( ctx , lbsu , service . Servers ) ; err != nil {
2018-11-14 09:18:03 +00:00
return nil , fmt . Errorf ( "error configuring load balancer for service %s: %v" , serviceName , err )
}
return lb , nil
}
2019-07-10 07:26:04 +00:00
func ( m * Manager ) upsertServers ( ctx context . Context , lb healthcheck . BalancerHandler , servers [ ] dynamic . Server ) error {
2018-11-14 09:18:03 +00:00
logger := log . FromContext ( ctx )
for name , srv := range servers {
u , err := url . Parse ( srv . URL )
if err != nil {
return fmt . Errorf ( "error parsing server URL %s: %v" , srv . URL , err )
}
2019-06-05 20:18:06 +00:00
logger . WithField ( log . ServerName , name ) . Debugf ( "Creating server %d %s" , name , u )
2018-11-14 09:18:03 +00:00
2019-06-05 20:18:06 +00:00
if err := lb . UpsertServer ( u , roundrobin . Weight ( 1 ) ) ; err != nil {
2018-11-14 09:18:03 +00:00
return fmt . Errorf ( "error adding server %s to load balancer: %v" , srv . URL , err )
}
// FIXME Handle Metrics
}
return nil
}