2017-12-02 18:26:44 +00:00
package docker
import (
2018-03-23 12:30:03 +00:00
"context"
2018-06-22 17:44:03 +00:00
"crypto/md5"
"encoding/hex"
2018-03-23 12:30:03 +00:00
"fmt"
2018-06-13 08:08:03 +00:00
"net"
2017-12-02 18:26:44 +00:00
"strconv"
2018-03-23 12:30:03 +00:00
"strings"
2017-12-02 18:26:44 +00:00
"text/template"
"github.com/BurntSushi/ty/fun"
"github.com/containous/traefik/log"
2018-03-23 12:30:03 +00:00
"github.com/containous/traefik/provider"
2017-12-02 18:26:44 +00:00
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
2018-03-23 12:30:03 +00:00
"github.com/docker/go-connections/nat"
2017-12-02 18:26:44 +00:00
)
2018-03-23 12:30:03 +00:00
const (
labelDockerNetwork = "traefik.docker.network"
labelBackendLoadBalancerSwarm = "traefik.backend.loadbalancer.swarm"
labelDockerComposeProject = "com.docker.compose.project"
labelDockerComposeService = "com.docker.compose.service"
)
2018-07-23 09:56:02 +00:00
func ( p * Provider ) buildConfiguration ( containersInspected [ ] dockerData ) * types . Configuration {
2018-03-23 12:30:03 +00:00
dockerFuncMap := template . FuncMap {
"getLabelValue" : label . GetStringValue ,
2018-01-09 15:26:03 +00:00
"getSubDomain" : getSubDomain ,
2018-03-23 12:30:03 +00:00
"isBackendLBSwarm" : isBackendLBSwarm ,
2018-03-26 13:32:04 +00:00
"getDomain" : label . GetFuncString ( label . TraefikDomain , p . Domain ) ,
2017-12-18 10:02:04 +00:00
// Backend functions
2018-07-19 14:40:03 +00:00
"getIPAddress" : p . getDeprecatedIPAddress , // TODO: Should we expose getIPPort instead?
2018-03-23 12:30:03 +00:00
"getServers" : p . getServers ,
2018-03-26 13:32:04 +00:00
"getMaxConn" : label . GetMaxConn ,
"getHealthCheck" : label . GetHealthCheck ,
"getBuffering" : label . GetBuffering ,
"getCircuitBreaker" : label . GetCircuitBreaker ,
"getLoadBalancer" : label . GetLoadBalancer ,
2018-01-09 15:26:03 +00:00
2017-12-18 10:02:04 +00:00
// Frontend functions
2018-08-29 09:36:03 +00:00
"getBackendName" : getBackendName ,
"getPriority" : label . GetFuncInt ( label . TraefikFrontendPriority , label . DefaultFrontendPriority ) ,
"getPassHostHeader" : label . GetFuncBool ( label . TraefikFrontendPassHostHeader , label . DefaultPassHostHeader ) ,
"getPassTLSCert" : label . GetFuncBool ( label . TraefikFrontendPassTLSCert , label . DefaultPassTLSCert ) ,
"getPassTLSClientCert" : label . GetTLSClientCert ,
"getEntryPoints" : label . GetFuncSliceString ( label . TraefikFrontendEntryPoints ) ,
"getBasicAuth" : label . GetFuncSliceString ( label . TraefikFrontendAuthBasic ) , // Deprecated
"getAuth" : label . GetAuth ,
"getFrontendRule" : p . getFrontendRule ,
"getRedirect" : label . GetRedirect ,
"getErrorPages" : label . GetErrorPages ,
"getRateLimit" : label . GetRateLimit ,
"getHeaders" : label . GetHeaders ,
"getWhiteList" : label . GetWhiteList ,
2017-12-02 18:26:44 +00:00
}
2018-03-23 12:30:03 +00:00
2017-12-02 18:26:44 +00:00
// filter containers
2018-03-23 12:30:03 +00:00
filteredContainers := fun . Filter ( p . containerFilter , containersInspected ) . ( [ ] dockerData )
2017-12-02 18:26:44 +00:00
frontends := map [ string ] [ ] dockerData { }
servers := map [ string ] [ ] dockerData { }
2018-03-23 12:30:03 +00:00
2017-12-02 18:26:44 +00:00
serviceNames := make ( map [ string ] struct { } )
2018-03-23 12:30:03 +00:00
2017-12-02 18:26:44 +00:00
for idx , container := range filteredContainers {
2018-03-23 12:30:03 +00:00
segmentProperties := label . ExtractTraefikLabels ( container . Labels )
for segmentName , labels := range segmentProperties {
container . SegmentLabels = labels
container . SegmentName = segmentName
2018-04-16 16:14:04 +00:00
serviceNamesKey := getServiceNameKey ( container , p . SwarmMode , segmentName )
if _ , exists := serviceNames [ serviceNamesKey ] ; ! exists {
2018-03-23 12:30:03 +00:00
frontendName := p . getFrontendName ( container , idx )
frontends [ frontendName ] = append ( frontends [ frontendName ] , container )
2018-04-16 16:14:04 +00:00
if len ( serviceNamesKey ) > 0 {
serviceNames [ serviceNamesKey ] = struct { } { }
2018-03-23 12:30:03 +00:00
}
2017-12-02 18:26:44 +00:00
}
2018-03-23 12:30:03 +00:00
// Backends
backendName := getBackendName ( container )
// Servers
servers [ backendName ] = append ( servers [ backendName ] , container )
2017-12-02 18:26:44 +00:00
}
}
templateObjects := struct {
Containers [ ] dockerData
Frontends map [ string ] [ ] dockerData
Servers map [ string ] [ ] dockerData
Domain string
} {
2017-12-15 21:16:48 +00:00
Containers : filteredContainers ,
Frontends : frontends ,
Servers : servers ,
Domain : p . Domain ,
2017-12-02 18:26:44 +00:00
}
2018-03-23 12:30:03 +00:00
configuration , err := p . GetConfiguration ( "templates/docker.tmpl" , dockerFuncMap , templateObjects )
2017-12-02 18:26:44 +00:00
if err != nil {
log . Error ( err )
}
return configuration
}
2018-04-16 16:14:04 +00:00
func getServiceNameKey ( container dockerData , swarmMode bool , segmentName string ) string {
2018-06-22 17:44:03 +00:00
if swarmMode {
return container . ServiceName + segmentName
2018-04-16 16:14:04 +00:00
}
2018-06-22 17:44:03 +00:00
return getServiceName ( container ) + segmentName
2018-04-16 16:14:04 +00:00
}
2018-03-23 12:30:03 +00:00
func ( p * Provider ) containerFilter ( container dockerData ) bool {
2017-12-02 18:26:44 +00:00
if ! label . IsEnabled ( container . Labels , p . ExposedByDefault ) {
log . Debugf ( "Filtering disabled container %s" , container . Name )
return false
}
2018-03-23 12:30:03 +00:00
segmentProperties := label . ExtractTraefikLabels ( container . Labels )
var errPort error
for segmentName , labels := range segmentProperties {
errPort = checkSegmentPort ( labels , segmentName )
2018-03-28 15:18:04 +00:00
if len ( p . getFrontendRule ( container , labels ) ) == 0 {
2018-03-23 12:30:03 +00:00
log . Debugf ( "Filtering container with empty frontend rule %s %s" , container . Name , segmentName )
return false
}
2017-12-02 18:26:44 +00:00
}
2018-03-23 12:30:03 +00:00
if len ( container . NetworkSettings . Ports ) == 0 && errPort != nil {
log . Debugf ( "Filtering container without port, %s: %v" , container . Name , errPort )
2017-12-02 18:26:44 +00:00
return false
}
constraintTags := label . SplitAndTrimString ( container . Labels [ label . TraefikTags ] , "," )
if ok , failingConstraint := p . MatchConstraints ( constraintTags ) ; ! ok {
if failingConstraint != nil {
2018-03-23 12:30:03 +00:00
log . Debugf ( "Container %s pruned by %q constraint" , container . Name , failingConstraint . String ( ) )
2017-12-02 18:26:44 +00:00
}
return false
}
if container . Health != "" && container . Health != "healthy" {
log . Debugf ( "Filtering unhealthy or starting container %s" , container . Name )
return false
}
2018-03-23 12:30:03 +00:00
return true
}
func checkSegmentPort ( labels map [ string ] string , segmentName string ) error {
if port , ok := labels [ label . TraefikPort ] ; ok {
_ , err := strconv . Atoi ( port )
if err != nil {
return fmt . Errorf ( "invalid port value %q for the segment %q: %v" , port , segmentName , err )
}
} else {
return fmt . Errorf ( "port label is missing, please use %s as default value or define port label for all segments ('traefik.<segment_name>.port')" , label . TraefikPort )
2017-12-02 18:26:44 +00:00
}
2018-03-23 12:30:03 +00:00
return nil
}
2017-12-02 18:26:44 +00:00
2018-03-23 12:30:03 +00:00
func ( p * Provider ) getFrontendName ( container dockerData , idx int ) string {
var name string
if len ( container . SegmentName ) > 0 {
2018-06-22 17:44:03 +00:00
name = container . SegmentName + "-" + getBackendName ( container )
2018-03-23 12:30:03 +00:00
} else {
2018-03-28 15:18:04 +00:00
name = p . getFrontendRule ( container , container . SegmentLabels ) + "-" + strconv . Itoa ( idx )
2018-03-23 12:30:03 +00:00
}
return provider . Normalize ( name )
}
2018-03-28 15:18:04 +00:00
func ( p * Provider ) getFrontendRule ( container dockerData , segmentLabels map [ string ] string ) string {
if value := label . GetStringValue ( segmentLabels , label . TraefikFrontendRule , "" ) ; len ( value ) != 0 {
2018-03-23 12:30:03 +00:00
return value
}
2018-04-17 18:58:24 +00:00
domain := label . GetStringValue ( segmentLabels , label . TraefikDomain , p . Domain )
2018-03-23 12:30:03 +00:00
if values , err := label . GetStringMultipleStrict ( container . Labels , labelDockerComposeProject , labelDockerComposeService ) ; err == nil {
2018-04-17 18:58:24 +00:00
return "Host:" + getSubDomain ( values [ labelDockerComposeService ] + "." + values [ labelDockerComposeProject ] ) + "." + domain
2018-03-23 12:30:03 +00:00
}
2018-04-17 18:58:24 +00:00
if len ( domain ) > 0 {
return "Host:" + getSubDomain ( container . ServiceName ) + "." + domain
2018-03-23 12:30:03 +00:00
}
return ""
}
func ( p Provider ) getIPAddress ( container dockerData ) string {
2018-06-13 12:50:04 +00:00
if value := label . GetStringValue ( container . Labels , labelDockerNetwork , p . Network ) ; value != "" {
2018-03-23 12:30:03 +00:00
networkSettings := container . NetworkSettings
if networkSettings . Networks != nil {
network := networkSettings . Networks [ value ]
if network != nil {
return network . Addr
}
log . Warnf ( "Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network." , value , container . Name )
}
}
if container . NetworkSettings . NetworkMode . IsHost ( ) {
if container . Node != nil {
if container . Node . IPAddress != "" {
return container . Node . IPAddress
}
}
return "127.0.0.1"
}
if container . NetworkSettings . NetworkMode . IsContainer ( ) {
dockerClient , err := p . createClient ( )
if err != nil {
log . Warnf ( "Unable to get IP address for container %s, error: %s" , container . Name , err )
return ""
}
connectedContainer := container . NetworkSettings . NetworkMode . ConnectedContainer ( )
containerInspected , err := dockerClient . ContainerInspect ( context . Background ( ) , connectedContainer )
if err != nil {
log . Warnf ( "Unable to get IP address for container %s : Failed to inspect container ID %s, error: %s" , container . Name , connectedContainer , err )
return ""
}
return p . getIPAddress ( parseContainer ( containerInspected ) )
}
for _ , network := range container . NetworkSettings . Networks {
return network . Addr
}
2018-04-22 07:10:03 +00:00
log . Warnf ( "Unable to find the IP address for the container %q." , container . Name )
2018-03-23 12:30:03 +00:00
return ""
}
2018-07-19 14:40:03 +00:00
// Deprecated: Please use getIPPort instead
func ( p * Provider ) getDeprecatedIPAddress ( container dockerData ) string {
ip , _ , err := p . getIPPort ( container )
if err != nil {
log . Warn ( err )
return ""
}
return ip
}
2018-03-23 12:30:03 +00:00
// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-"
func getSubDomain ( name string ) string {
return strings . Replace ( strings . Replace ( strings . TrimPrefix ( name , "/" ) , "/" , "-" , - 1 ) , "_" , "-" , - 1 )
}
func isBackendLBSwarm ( container dockerData ) bool {
return label . GetBoolValue ( container . Labels , labelBackendLoadBalancerSwarm , false )
}
2018-06-22 17:44:03 +00:00
func getBackendName ( container dockerData ) string {
if len ( container . SegmentName ) > 0 {
return getSegmentBackendName ( container )
2018-06-14 07:20:04 +00:00
}
2018-06-22 17:44:03 +00:00
return getDefaultBackendName ( container )
}
func getSegmentBackendName ( container dockerData ) string {
serviceName := getServiceName ( container )
2018-05-14 08:18:03 +00:00
if value := label . GetStringValue ( container . SegmentLabels , label . TraefikBackend , "" ) ; len ( value ) > 0 {
2018-06-14 07:20:04 +00:00
return provider . Normalize ( serviceName + "-" + value )
2018-03-23 12:30:03 +00:00
}
2018-06-22 17:44:03 +00:00
return provider . Normalize ( serviceName + "-" + container . SegmentName )
2018-03-23 12:30:03 +00:00
}
func getDefaultBackendName ( container dockerData ) string {
if value := label . GetStringValue ( container . SegmentLabels , label . TraefikBackend , "" ) ; len ( value ) != 0 {
return provider . Normalize ( value )
}
2018-06-22 17:44:03 +00:00
return provider . Normalize ( getServiceName ( container ) )
2018-03-23 12:30:03 +00:00
}
2018-06-22 17:44:03 +00:00
func getServiceName ( container dockerData ) string {
serviceName := container . ServiceName
if values , err := label . GetStringMultipleStrict ( container . Labels , labelDockerComposeProject , labelDockerComposeService ) ; err == nil {
serviceName = values [ labelDockerComposeService ] + "_" + values [ labelDockerComposeProject ]
2018-03-23 12:30:03 +00:00
}
2018-06-22 17:44:03 +00:00
return serviceName
2018-03-23 12:30:03 +00:00
}
func getPort ( container dockerData ) string {
if value := label . GetStringValue ( container . SegmentLabels , label . TraefikPort , "" ) ; len ( value ) != 0 {
return value
}
// See iteration order in https://blog.golang.org/go-maps-in-action
var ports [ ] nat . Port
for port := range container . NetworkSettings . Ports {
ports = append ( ports , port )
}
less := func ( i , j nat . Port ) bool {
return i . Int ( ) < j . Int ( )
}
nat . Sort ( ports , less )
if len ( ports ) > 0 {
min := ports [ 0 ]
return min . Port ( )
}
return ""
}
2018-07-19 14:40:03 +00:00
func ( p * Provider ) getPortBinding ( container dockerData ) ( * nat . PortBinding , error ) {
port := getPort ( container )
for netPort , portBindings := range container . NetworkSettings . Ports {
if strings . EqualFold ( string ( netPort ) , port + "/TCP" ) || strings . EqualFold ( string ( netPort ) , port + "/UDP" ) {
for _ , p := range portBindings {
return & p , nil
}
}
}
return nil , fmt . Errorf ( "unable to find the external IP:Port for the container %q" , container . Name )
}
func ( p * Provider ) getIPPort ( container dockerData ) ( string , string , error ) {
var ip , port string
if p . UseBindPortIP {
portBinding , err := p . getPortBinding ( container )
if err != nil {
return "" , "" , fmt . Errorf ( "unable to find a binding for the container %q: ignoring server" , container . Name )
}
if portBinding . HostIP == "0.0.0.0" {
return "" , "" , fmt . Errorf ( "cannot determine the IP address (got 0.0.0.0) for the container %q: ignoring server" , container . Name )
}
ip = portBinding . HostIP
port = portBinding . HostPort
} else {
ip = p . getIPAddress ( container )
port = getPort ( container )
}
if len ( ip ) == 0 {
return "" , "" , fmt . Errorf ( "unable to find the IP address for the container %q: the server is ignored" , container . Name )
}
return ip , port , nil
}
2018-03-23 12:30:03 +00:00
func ( p * Provider ) getServers ( containers [ ] dockerData ) map [ string ] types . Server {
var servers map [ string ] types . Server
2018-06-22 17:44:03 +00:00
for _ , container := range containers {
2018-07-19 14:40:03 +00:00
ip , port , err := p . getIPPort ( container )
if err != nil {
log . Warn ( err )
2018-04-22 07:10:03 +00:00
continue
}
2018-03-23 12:30:03 +00:00
if servers == nil {
servers = make ( map [ string ] types . Server )
}
protocol := label . GetStringValue ( container . SegmentLabels , label . TraefikProtocol , label . DefaultProtocol )
2018-06-22 17:44:03 +00:00
serverURL := fmt . Sprintf ( "%s://%s" , protocol , net . JoinHostPort ( ip , port ) )
serverName := getServerName ( container . Name , serverURL )
if _ , exist := servers [ serverName ] ; exist {
log . Debugf ( "Skipping server %q with the same URL." , serverName )
continue
2018-03-23 12:30:03 +00:00
}
2018-06-22 17:44:03 +00:00
servers [ serverName ] = types . Server {
URL : serverURL ,
2018-04-11 14:30:04 +00:00
Weight : label . GetIntValue ( container . SegmentLabels , label . TraefikWeight , label . DefaultWeight ) ,
2018-03-23 12:30:03 +00:00
}
}
return servers
}
2018-06-22 17:44:03 +00:00
func getServerName ( containerName , url string ) string {
hash := md5 . New ( )
_ , err := hash . Write ( [ ] byte ( url ) )
if err != nil {
// Impossible case
log . Errorf ( "Fail to hash server URL %q" , url )
}
return provider . Normalize ( "server-" + containerName + "-" + hex . EncodeToString ( hash . Sum ( nil ) ) )
}