2023-05-10 13:28:05 +00:00
package docker
import (
"context"
"fmt"
"net/http"
"text/template"
"time"
"github.com/docker/cli/cli/connhelper"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/docker/go-connections/sockets"
"github.com/rs/zerolog/log"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/types"
"github.com/traefik/traefik/v3/pkg/version"
)
// DefaultTemplateRule The default template for the default rule.
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
type Shared struct {
ExposedByDefault bool ` description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true" `
Constraints string ` description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true" `
AllowEmptyServices bool ` description:"Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true" `
Network string ` description:"Default Docker network used." json:"network,omitempty" toml:"network,omitempty" yaml:"network,omitempty" export:"true" `
UseBindPortIP bool ` description:"Use the ip address from the bound port, rather than from the inner network." json:"useBindPortIP,omitempty" toml:"useBindPortIP,omitempty" yaml:"useBindPortIP,omitempty" export:"true" `
Watch bool ` description:"Watch Docker events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true" `
DefaultRule string ` description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty" `
defaultRuleTpl * template . Template
}
func inspectContainers ( ctx context . Context , dockerClient client . ContainerAPIClient , containerID string ) dockerData {
containerInspected , err := dockerClient . ContainerInspect ( ctx , containerID )
if err != nil {
log . Ctx ( ctx ) . Warn ( ) . Err ( err ) . Msgf ( "Failed to inspect container %s" , containerID )
return dockerData { }
}
// This condition is here to avoid to have empty IP https://github.com/traefik/traefik/issues/2459
// We register only container which are running
if containerInspected . ContainerJSONBase != nil && containerInspected . ContainerJSONBase . State != nil && containerInspected . ContainerJSONBase . State . Running {
return parseContainer ( containerInspected )
}
return dockerData { }
}
func parseContainer ( container dockertypes . ContainerJSON ) dockerData {
dData := dockerData {
NetworkSettings : networkSettings { } ,
}
if container . ContainerJSONBase != nil {
dData . ID = container . ContainerJSONBase . ID
dData . Name = container . ContainerJSONBase . Name
dData . ServiceName = dData . Name // Default ServiceName to be the container's Name.
dData . Node = container . ContainerJSONBase . Node
if container . ContainerJSONBase . HostConfig != nil {
dData . NetworkSettings . NetworkMode = container . ContainerJSONBase . HostConfig . NetworkMode
}
if container . State != nil && container . State . Health != nil {
dData . Health = container . State . Health . Status
}
}
if container . Config != nil && container . Config . Labels != nil {
dData . Labels = container . Config . Labels
}
if container . NetworkSettings != nil {
if container . NetworkSettings . Ports != nil {
dData . NetworkSettings . Ports = container . NetworkSettings . Ports
}
if container . NetworkSettings . Networks != nil {
dData . NetworkSettings . Networks = make ( map [ string ] * networkData )
for name , containerNetwork := range container . NetworkSettings . Networks {
addr := containerNetwork . IPAddress
if addr == "" {
addr = containerNetwork . GlobalIPv6Address
}
dData . NetworkSettings . Networks [ name ] = & networkData {
ID : containerNetwork . NetworkID ,
Name : name ,
Addr : addr ,
}
}
}
}
return dData
}
type ClientConfig struct {
apiVersion string
Endpoint string ` description:"Docker server endpoint. Can be a TCP or a Unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" `
TLS * types . ClientTLS ` description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true" `
HTTPClientTimeout ptypes . Duration ` description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true" `
}
func createClient ( ctx context . Context , cfg ClientConfig ) ( * client . Client , error ) {
opts , err := getClientOpts ( ctx , cfg )
if err != nil {
return nil , err
}
httpHeaders := map [ string ] string {
"User-Agent" : "Traefik " + version . Version ,
}
opts = append ( opts ,
client . WithHTTPHeaders ( httpHeaders ) ,
client . WithVersion ( cfg . apiVersion ) )
return client . NewClientWithOpts ( opts ... )
}
func getClientOpts ( ctx context . Context , cfg ClientConfig ) ( [ ] client . Opt , error ) {
helper , err := connhelper . GetConnectionHelper ( cfg . Endpoint )
if err != nil {
return nil , err
}
// SSH
if helper != nil {
// https://github.com/docker/cli/blob/ebca1413117a3fcb81c89d6be226dcec74e5289f/cli/context/docker/load.go#L112-L123
httpClient := & http . Client {
Transport : & http . Transport {
DialContext : helper . Dialer ,
} ,
}
return [ ] client . Opt {
client . WithHTTPClient ( httpClient ) ,
client . WithTimeout ( time . Duration ( cfg . HTTPClientTimeout ) ) ,
client . WithHost ( helper . Host ) , // To avoid 400 Bad Request: malformed Host header daemon error
client . WithDialContext ( helper . Dialer ) ,
} , nil
}
opts := [ ] client . Opt {
client . WithHost ( cfg . Endpoint ) ,
client . WithTimeout ( time . Duration ( cfg . HTTPClientTimeout ) ) ,
}
if cfg . TLS != nil {
conf , err := cfg . TLS . CreateTLSConfig ( ctx )
if err != nil {
return nil , fmt . Errorf ( "unable to create client TLS configuration: %w" , err )
}
hostURL , err := client . ParseHostURL ( cfg . Endpoint )
if err != nil {
return nil , err
}
tr := & http . Transport {
TLSClientConfig : conf ,
}
if err := sockets . ConfigureTransport ( tr , hostURL . Scheme , hostURL . Host ) ; err != nil {
return nil , err
}
opts = append ( opts , client . WithHTTPClient ( & http . Client { Transport : tr , Timeout : time . Duration ( cfg . HTTPClientTimeout ) } ) )
}
return opts , nil
}
func getPort ( container dockerData , serverPort string ) string {
if len ( serverPort ) > 0 {
return serverPort
}
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 {
2024-08-28 14:11:38 +00:00
return ports [ 0 ] . Port ( )
2023-05-10 13:28:05 +00:00
}
return ""
}
func getServiceName ( container dockerData ) string {
serviceName := container . ServiceName
if values , err := getStringMultipleStrict ( container . Labels , labelDockerComposeProject , labelDockerComposeService ) ; err == nil {
serviceName = values [ labelDockerComposeService ] + "_" + values [ labelDockerComposeProject ]
}
return provider . Normalize ( serviceName )
}