Move code to pkg
This commit is contained in:
parent
bd4c822670
commit
f1b085fa36
465 changed files with 656 additions and 680 deletions
214
pkg/provider/docker/builder_test.go
Normal file
214
pkg/provider/docker/builder_test.go
Normal file
|
@ -0,0 +1,214 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
docker "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
func containerJSON(ops ...func(*docker.ContainerJSON)) docker.ContainerJSON {
|
||||
c := &docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "fake",
|
||||
HostConfig: &container.HostConfig{},
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
op(c)
|
||||
}
|
||||
|
||||
return *c
|
||||
}
|
||||
|
||||
func name(name string) func(*docker.ContainerJSON) {
|
||||
return func(c *docker.ContainerJSON) {
|
||||
c.ContainerJSONBase.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func networkMode(mode string) func(*docker.ContainerJSON) {
|
||||
return func(c *docker.ContainerJSON) {
|
||||
c.ContainerJSONBase.HostConfig.NetworkMode = container.NetworkMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
func nodeIP(ip string) func(*docker.ContainerJSON) {
|
||||
return func(c *docker.ContainerJSON) {
|
||||
c.ContainerJSONBase.Node = &docker.ContainerNode{
|
||||
IPAddress: ip,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func labels(labels map[string]string) func(*docker.ContainerJSON) {
|
||||
return func(c *docker.ContainerJSON) {
|
||||
c.Config.Labels = labels
|
||||
}
|
||||
}
|
||||
|
||||
func ports(portMap nat.PortMap) func(*docker.ContainerJSON) {
|
||||
return func(c *docker.ContainerJSON) {
|
||||
c.NetworkSettings.NetworkSettingsBase.Ports = portMap
|
||||
}
|
||||
}
|
||||
|
||||
func withNetwork(name string, ops ...func(*network.EndpointSettings)) func(*docker.ContainerJSON) {
|
||||
return func(c *docker.ContainerJSON) {
|
||||
if c.NetworkSettings.Networks == nil {
|
||||
c.NetworkSettings.Networks = map[string]*network.EndpointSettings{}
|
||||
}
|
||||
c.NetworkSettings.Networks[name] = &network.EndpointSettings{}
|
||||
for _, op := range ops {
|
||||
op(c.NetworkSettings.Networks[name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ipv4(ip string) func(*network.EndpointSettings) {
|
||||
return func(s *network.EndpointSettings) {
|
||||
s.IPAddress = ip
|
||||
}
|
||||
}
|
||||
|
||||
func swarmTask(id string, ops ...func(*swarm.Task)) swarm.Task {
|
||||
task := &swarm.Task{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
op(task)
|
||||
}
|
||||
|
||||
return *task
|
||||
}
|
||||
|
||||
func taskSlot(slot int) func(*swarm.Task) {
|
||||
return func(task *swarm.Task) {
|
||||
task.Slot = slot
|
||||
}
|
||||
}
|
||||
|
||||
func taskNetworkAttachment(id string, name string, driver string, addresses []string) func(*swarm.Task) {
|
||||
return func(task *swarm.Task) {
|
||||
task.NetworksAttachments = append(task.NetworksAttachments, swarm.NetworkAttachment{
|
||||
Network: swarm.Network{
|
||||
ID: id,
|
||||
Spec: swarm.NetworkSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
},
|
||||
DriverConfiguration: &swarm.Driver{
|
||||
Name: driver,
|
||||
},
|
||||
},
|
||||
},
|
||||
Addresses: addresses,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func taskStatus(ops ...func(*swarm.TaskStatus)) func(*swarm.Task) {
|
||||
return func(task *swarm.Task) {
|
||||
status := &swarm.TaskStatus{}
|
||||
|
||||
for _, op := range ops {
|
||||
op(status)
|
||||
}
|
||||
|
||||
task.Status = *status
|
||||
}
|
||||
}
|
||||
|
||||
func taskState(state swarm.TaskState) func(*swarm.TaskStatus) {
|
||||
return func(status *swarm.TaskStatus) {
|
||||
status.State = state
|
||||
}
|
||||
}
|
||||
|
||||
func taskContainerStatus(id string) func(*swarm.TaskStatus) {
|
||||
return func(status *swarm.TaskStatus) {
|
||||
status.ContainerStatus = swarm.ContainerStatus{
|
||||
ContainerID: id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func swarmService(ops ...func(*swarm.Service)) swarm.Service {
|
||||
service := &swarm.Service{
|
||||
ID: "serviceID",
|
||||
Spec: swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: "defaultServiceName",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
op(service)
|
||||
}
|
||||
|
||||
return *service
|
||||
}
|
||||
|
||||
func serviceName(name string) func(service *swarm.Service) {
|
||||
return func(service *swarm.Service) {
|
||||
service.Spec.Annotations.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func serviceLabels(labels map[string]string) func(service *swarm.Service) {
|
||||
return func(service *swarm.Service) {
|
||||
service.Spec.Annotations.Labels = labels
|
||||
}
|
||||
}
|
||||
|
||||
func withEndpoint(ops ...func(*swarm.Endpoint)) func(*swarm.Service) {
|
||||
return func(service *swarm.Service) {
|
||||
endpoint := &swarm.Endpoint{}
|
||||
|
||||
for _, op := range ops {
|
||||
op(endpoint)
|
||||
}
|
||||
|
||||
service.Endpoint = *endpoint
|
||||
}
|
||||
}
|
||||
|
||||
func virtualIP(networkID, addr string) func(*swarm.Endpoint) {
|
||||
return func(endpoint *swarm.Endpoint) {
|
||||
if endpoint.VirtualIPs == nil {
|
||||
endpoint.VirtualIPs = []swarm.EndpointVirtualIP{}
|
||||
}
|
||||
endpoint.VirtualIPs = append(endpoint.VirtualIPs, swarm.EndpointVirtualIP{
|
||||
NetworkID: networkID,
|
||||
Addr: addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func withEndpointSpec(ops ...func(*swarm.EndpointSpec)) func(*swarm.Service) {
|
||||
return func(service *swarm.Service) {
|
||||
endpointSpec := &swarm.EndpointSpec{}
|
||||
|
||||
for _, op := range ops {
|
||||
op(endpointSpec)
|
||||
}
|
||||
|
||||
service.Spec.EndpointSpec = endpointSpec
|
||||
}
|
||||
}
|
||||
|
||||
func modeDNSSR(spec *swarm.EndpointSpec) {
|
||||
spec.Mode = swarm.ResolutionModeDNSRR
|
||||
}
|
||||
|
||||
func modeVIP(spec *swarm.EndpointSpec) {
|
||||
spec.Mode = swarm.ResolutionModeVIP
|
||||
}
|
263
pkg/provider/docker/config.go
Normal file
263
pkg/provider/docker/config.go
Normal file
|
@ -0,0 +1,263 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/pkg/config"
|
||||
"github.com/containous/traefik/pkg/log"
|
||||
"github.com/containous/traefik/pkg/provider"
|
||||
"github.com/containous/traefik/pkg/provider/label"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
func (p *Provider) buildConfiguration(ctx context.Context, containersInspected []dockerData) *config.Configuration {
|
||||
configurations := make(map[string]*config.Configuration)
|
||||
|
||||
for _, container := range containersInspected {
|
||||
containerName := getServiceName(container) + "-" + container.ID
|
||||
ctxContainer := log.With(ctx, log.Str("container", containerName))
|
||||
|
||||
if !p.keepContainer(ctxContainer, container) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger := log.FromContext(ctxContainer)
|
||||
|
||||
confFromLabel, err := label.DecodeConfiguration(container.Labels)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = p.buildServiceConfiguration(ctxContainer, container, confFromLabel.HTTP)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
serviceName := getServiceName(container)
|
||||
|
||||
model := struct {
|
||||
Name string
|
||||
Labels map[string]string
|
||||
}{
|
||||
Name: serviceName,
|
||||
Labels: container.Labels,
|
||||
}
|
||||
|
||||
provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, serviceName, p.defaultRuleTpl, model)
|
||||
|
||||
configurations[containerName] = confFromLabel
|
||||
}
|
||||
|
||||
return provider.Merge(ctx, configurations)
|
||||
}
|
||||
|
||||
func (p *Provider) buildServiceConfiguration(ctx context.Context, container dockerData, configuration *config.HTTPConfiguration) error {
|
||||
serviceName := getServiceName(container)
|
||||
|
||||
if len(configuration.Services) == 0 {
|
||||
configuration.Services = make(map[string]*config.Service)
|
||||
lb := &config.LoadBalancerService{}
|
||||
lb.SetDefaults()
|
||||
configuration.Services[serviceName] = &config.Service{
|
||||
LoadBalancer: lb,
|
||||
}
|
||||
}
|
||||
|
||||
for _, service := range configuration.Services {
|
||||
err := p.addServer(ctx, container, service.LoadBalancer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
if !container.ExtraConf.Enable {
|
||||
logger.Debug("Filtering disabled container")
|
||||
return false
|
||||
}
|
||||
|
||||
if ok, failingConstraint := p.MatchConstraints(container.ExtraConf.Tags); !ok {
|
||||
if failingConstraint != nil {
|
||||
logger.Debugf("Container pruned by %q constraint", failingConstraint.String())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if container.Health != "" && container.Health != "healthy" {
|
||||
logger.Debug("Filtering unhealthy or starting container")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Provider) addServer(ctx context.Context, container dockerData, loadBalancer *config.LoadBalancerService) error {
|
||||
serverPort := getLBServerPort(loadBalancer)
|
||||
ip, port, err := p.getIPPort(ctx, container, serverPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(loadBalancer.Servers) == 0 {
|
||||
server := config.Server{}
|
||||
server.SetDefaults()
|
||||
|
||||
loadBalancer.Servers = []config.Server{server}
|
||||
}
|
||||
|
||||
if serverPort != "" {
|
||||
port = serverPort
|
||||
loadBalancer.Servers[0].Port = ""
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
return errors.New("port is missing")
|
||||
}
|
||||
|
||||
loadBalancer.Servers[0].URL = fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(ip, port))
|
||||
loadBalancer.Servers[0].Scheme = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) getIPPort(ctx context.Context, container dockerData, serverPort string) (string, string, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var ip, port string
|
||||
usedBound := false
|
||||
|
||||
if p.UseBindPortIP {
|
||||
portBinding, err := p.getPortBinding(container, serverPort)
|
||||
switch {
|
||||
case err != nil:
|
||||
logger.Infof("Unable to find a binding for container %q, falling back on its internal IP/Port.", container.Name)
|
||||
case portBinding.HostIP == "0.0.0.0" || len(portBinding.HostIP) == 0:
|
||||
logger.Infof("Cannot determine the IP address (got %q) for %q's binding, falling back on its internal IP/Port.", portBinding.HostIP, container.Name)
|
||||
default:
|
||||
ip = portBinding.HostIP
|
||||
port = portBinding.HostPort
|
||||
usedBound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !usedBound {
|
||||
ip = p.getIPAddress(ctx, container)
|
||||
port = getPort(container, serverPort)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p Provider) getIPAddress(ctx context.Context, container dockerData) string {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
if container.ExtraConf.Docker.Network != "" {
|
||||
settings := container.NetworkSettings
|
||||
if settings.Networks != nil {
|
||||
network := settings.Networks[container.ExtraConf.Docker.Network]
|
||||
if network != nil {
|
||||
return network.Addr
|
||||
}
|
||||
|
||||
logger.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.", container.ExtraConf.Docker.Network, container.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if container.NetworkSettings.NetworkMode.IsHost() {
|
||||
if container.Node != nil && container.Node.IPAddress != "" {
|
||||
return container.Node.IPAddress
|
||||
}
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
if container.NetworkSettings.NetworkMode.IsContainer() {
|
||||
dockerClient, err := p.createClient()
|
||||
if err != nil {
|
||||
logger.Warnf("Unable to get IP address: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer()
|
||||
containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer)
|
||||
if err != nil {
|
||||
logger.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(ctx, parseContainer(containerInspected))
|
||||
}
|
||||
|
||||
for _, network := range container.NetworkSettings.Networks {
|
||||
return network.Addr
|
||||
}
|
||||
|
||||
logger.Warn("Unable to find the IP address.")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Provider) getPortBinding(container dockerData, serverPort string) (*nat.PortBinding, error) {
|
||||
port := getPort(container, serverPort)
|
||||
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 getLBServerPort(loadBalancer *config.LoadBalancerService) string {
|
||||
if loadBalancer != nil && len(loadBalancer.Servers) > 0 {
|
||||
return loadBalancer.Servers[0].Port
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 {
|
||||
min := ports[0]
|
||||
return min.Port()
|
||||
}
|
||||
|
||||
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 serviceName
|
||||
}
|
2393
pkg/provider/docker/config_test.go
Normal file
2393
pkg/provider/docker/config_test.go
Normal file
File diff suppressed because it is too large
Load diff
537
pkg/provider/docker/docker.go
Normal file
537
pkg/provider/docker/docker.go
Normal file
|
@ -0,0 +1,537 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/containous/traefik/pkg/config"
|
||||
"github.com/containous/traefik/pkg/job"
|
||||
"github.com/containous/traefik/pkg/log"
|
||||
"github.com/containous/traefik/pkg/provider"
|
||||
"github.com/containous/traefik/pkg/safe"
|
||||
"github.com/containous/traefik/pkg/types"
|
||||
"github.com/containous/traefik/pkg/version"
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
dockercontainertypes "github.com/docker/docker/api/types/container"
|
||||
eventtypes "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
)
|
||||
|
||||
const (
|
||||
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use.
|
||||
SwarmAPIVersion = "1.24"
|
||||
// DefaultTemplateRule The default template for the default rule.
|
||||
DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
||||
)
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
type Provider struct {
|
||||
provider.BaseProvider `mapstructure:",squash" export:"true"`
|
||||
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"`
|
||||
DefaultRule string `description:"Default rule"`
|
||||
TLS *types.ClientTLS `description:"Enable Docker TLS support" export:"true"`
|
||||
ExposedByDefault bool `description:"Expose containers by default" export:"true"`
|
||||
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network" export:"true"`
|
||||
SwarmMode bool `description:"Use Docker on Swarm Mode" export:"true"`
|
||||
Network string `description:"Default Docker network used" export:"true"`
|
||||
SwarmModeRefreshSeconds int `description:"Polling interval for swarm mode (in seconds)" export:"true"`
|
||||
defaultRuleTpl *template.Template
|
||||
}
|
||||
|
||||
// Init the provider.
|
||||
func (p *Provider) Init() error {
|
||||
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing default rule: %v", err)
|
||||
}
|
||||
|
||||
p.defaultRuleTpl = defaultRuleTpl
|
||||
return p.BaseProvider.Init()
|
||||
}
|
||||
|
||||
// dockerData holds the need data to the provider.
|
||||
type dockerData struct {
|
||||
ID string
|
||||
ServiceName string
|
||||
Name string
|
||||
Labels map[string]string // List of labels set to container or service
|
||||
NetworkSettings networkSettings
|
||||
Health string
|
||||
Node *dockertypes.ContainerNode
|
||||
ExtraConf configuration
|
||||
}
|
||||
|
||||
// NetworkSettings holds the networks data to the provider.
|
||||
type networkSettings struct {
|
||||
NetworkMode dockercontainertypes.NetworkMode
|
||||
Ports nat.PortMap
|
||||
Networks map[string]*networkData
|
||||
}
|
||||
|
||||
// Network holds the network data to the provider.
|
||||
type networkData struct {
|
||||
Name string
|
||||
Addr string
|
||||
Port int
|
||||
Protocol string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (p *Provider) createClient() (client.APIClient, error) {
|
||||
var httpClient *http.Client
|
||||
|
||||
if p.TLS != nil {
|
||||
ctx := log.With(context.Background(), log.Str(log.ProviderName, "docker"))
|
||||
conf, err := p.TLS.CreateTLSConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: conf,
|
||||
}
|
||||
|
||||
hostURL, err := client.ParseHostURL(p.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
|
||||
httpHeaders := map[string]string{
|
||||
"User-Agent": "Traefik " + version.Version,
|
||||
}
|
||||
|
||||
var apiVersion string
|
||||
if p.SwarmMode {
|
||||
apiVersion = SwarmAPIVersion
|
||||
} else {
|
||||
apiVersion = DockerAPIVersion
|
||||
}
|
||||
|
||||
return client.NewClient(p.Endpoint, apiVersion, httpClient, httpHeaders)
|
||||
}
|
||||
|
||||
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.Pool) error {
|
||||
pool.GoCtx(func(routineCtx context.Context) {
|
||||
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "docker"))
|
||||
logger := log.FromContext(ctxLog)
|
||||
|
||||
operation := func() error {
|
||||
var err error
|
||||
ctx, cancel := context.WithCancel(ctxLog)
|
||||
defer cancel()
|
||||
|
||||
ctx = log.With(ctx, log.Str(log.ProviderName, "docker"))
|
||||
|
||||
dockerClient, err := p.createClient()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to create a client for docker, error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
serverVersion, err := dockerClient.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to retrieve information of the docker client and server host: %s", err)
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)
|
||||
var dockerDataList []dockerData
|
||||
if p.SwarmMode {
|
||||
dockerDataList, err = p.listServices(ctx, dockerClient)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to list services for docker swarm mode, error %s", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dockerDataList, err = p.listContainers(ctx, dockerClient)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to list containers for docker, error %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configuration := p.buildConfiguration(ctxLog, dockerDataList)
|
||||
configurationChan <- config.Message{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
if p.Watch {
|
||||
if p.SwarmMode {
|
||||
errChan := make(chan error)
|
||||
// TODO: This need to be change. Linked to Swarm events docker/docker#23827
|
||||
ticker := time.NewTicker(time.Second * time.Duration(p.SwarmModeRefreshSeconds))
|
||||
pool.GoCtx(func(ctx context.Context) {
|
||||
|
||||
ctx = log.With(ctx, log.Str(log.ProviderName, "docker"))
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
defer close(errChan)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
services, err := p.listServices(ctx, dockerClient)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to list services for docker, error %s", err)
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
configuration := p.buildConfiguration(ctx, services)
|
||||
if configuration != nil {
|
||||
configurationChan <- config.Message{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if err, ok := <-errChan; ok {
|
||||
return err
|
||||
}
|
||||
// channel closed
|
||||
|
||||
} else {
|
||||
f := filters.NewArgs()
|
||||
f.Add("type", "container")
|
||||
options := dockertypes.EventsOptions{
|
||||
Filters: f,
|
||||
}
|
||||
|
||||
startStopHandle := func(m eventtypes.Message) {
|
||||
logger.Debugf("Provider event received %+v", m)
|
||||
containers, err := p.listContainers(ctx, dockerClient)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to list containers for docker, error %s", err)
|
||||
// Call cancel to get out of the monitor
|
||||
return
|
||||
}
|
||||
|
||||
configuration := p.buildConfiguration(ctx, containers)
|
||||
if configuration != nil {
|
||||
message := config.Message{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
select {
|
||||
case configurationChan <- message:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
eventsc, errc := dockerClient.Events(ctx, options)
|
||||
for {
|
||||
select {
|
||||
case event := <-eventsc:
|
||||
if event.Action == "start" ||
|
||||
event.Action == "die" ||
|
||||
strings.HasPrefix(event.Action, "health_status") {
|
||||
startStopHandle(event)
|
||||
}
|
||||
case err := <-errc:
|
||||
if err == io.EOF {
|
||||
logger.Debug("Provider event stream closed")
|
||||
}
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
notify := func(err error, time time.Duration) {
|
||||
logger.Errorf("Provider connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot connect to docker server %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) {
|
||||
containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var inspectedContainers []dockerData
|
||||
// get inspect containers
|
||||
for _, container := range containerList {
|
||||
dData := inspectContainers(ctx, dockerClient, container.ID)
|
||||
if len(dData.Name) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
extraConf, err := p.getConfiguration(dData)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Errorf("Skip container %s: %v", getServiceName(dData), err)
|
||||
continue
|
||||
}
|
||||
dData.ExtraConf = extraConf
|
||||
|
||||
inspectedContainers = append(inspectedContainers, dData)
|
||||
}
|
||||
return inspectedContainers, nil
|
||||
}
|
||||
|
||||
func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData {
|
||||
containerInspected, err := dockerClient.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Warnf("Failed to inspect container %s, error: %s", containerID, err)
|
||||
return dockerData{}
|
||||
}
|
||||
|
||||
// This condition is here to avoid to have empty IP https://github.com/containous/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 {
|
||||
dData.NetworkSettings.Networks[name] = &networkData{
|
||||
ID: containerNetwork.NetworkID,
|
||||
Name: name,
|
||||
Addr: containerNetwork.IPAddress,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dData
|
||||
}
|
||||
|
||||
func (p *Provider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverVersion, err := dockerClient.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
networkListArgs := filters.NewArgs()
|
||||
// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
|
||||
if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
|
||||
networkListArgs.Add("scope", "swarm")
|
||||
} else {
|
||||
networkListArgs.Add("driver", "overlay")
|
||||
}
|
||||
|
||||
networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs})
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to network inspect on client for docker, error: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
networkMap := make(map[string]*dockertypes.NetworkResource)
|
||||
for _, network := range networkList {
|
||||
networkToAdd := network
|
||||
networkMap[network.ID] = &networkToAdd
|
||||
}
|
||||
|
||||
var dockerDataList []dockerData
|
||||
var dockerDataListTasks []dockerData
|
||||
|
||||
for _, service := range serviceList {
|
||||
dData, err := p.parseService(ctx, service, networkMap)
|
||||
if err != nil {
|
||||
logger.Errorf("Skip container %s: %v", getServiceName(dData), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if dData.ExtraConf.Docker.LBSwarm {
|
||||
if len(dData.NetworkSettings.Networks) > 0 {
|
||||
dockerDataList = append(dockerDataList, dData)
|
||||
}
|
||||
} else {
|
||||
isGlobalSvc := service.Spec.Mode.Global != nil
|
||||
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc)
|
||||
if err != nil {
|
||||
logger.Warn(err)
|
||||
} else {
|
||||
dockerDataList = append(dockerDataList, dockerDataListTasks...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dockerDataList, err
|
||||
}
|
||||
|
||||
func (p *Provider) parseService(ctx context.Context, service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) (dockerData, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
dData := dockerData{
|
||||
ID: service.ID,
|
||||
ServiceName: service.Spec.Annotations.Name,
|
||||
Name: service.Spec.Annotations.Name,
|
||||
Labels: service.Spec.Annotations.Labels,
|
||||
NetworkSettings: networkSettings{},
|
||||
}
|
||||
|
||||
extraConf, err := p.getConfiguration(dData)
|
||||
if err != nil {
|
||||
return dockerData{}, err
|
||||
}
|
||||
dData.ExtraConf = extraConf
|
||||
|
||||
if service.Spec.EndpointSpec != nil {
|
||||
if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR {
|
||||
if dData.ExtraConf.Docker.LBSwarm {
|
||||
logger.Warnf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name)
|
||||
}
|
||||
} else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP {
|
||||
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||
for _, virtualIP := range service.Endpoint.VirtualIPs {
|
||||
networkService := networkMap[virtualIP.NetworkID]
|
||||
if networkService != nil {
|
||||
if len(virtualIP.Addr) > 0 {
|
||||
ip, _, _ := net.ParseCIDR(virtualIP.Addr)
|
||||
network := &networkData{
|
||||
Name: networkService.Name,
|
||||
ID: virtualIP.NetworkID,
|
||||
Addr: ip.String(),
|
||||
}
|
||||
dData.NetworkSettings.Networks[network.Name] = network
|
||||
} else {
|
||||
logger.Debugf("No virtual IPs found in network %s", virtualIP.NetworkID)
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("Network not found, id: %s", virtualIP.NetworkID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dData, nil
|
||||
}
|
||||
|
||||
func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
|
||||
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) ([]dockerData, error) {
|
||||
serviceIDFilter := filters.NewArgs()
|
||||
serviceIDFilter.Add("service", serviceID)
|
||||
serviceIDFilter.Add("desired-state", "running")
|
||||
|
||||
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dockerDataList []dockerData
|
||||
for _, task := range taskList {
|
||||
if task.Status.State != swarmtypes.TaskStateRunning {
|
||||
continue
|
||||
}
|
||||
dData := parseTasks(ctx, task, serviceDockerData, networkMap, isGlobalSvc)
|
||||
if len(dData.NetworkSettings.Networks) > 0 {
|
||||
dockerDataList = append(dockerDataList, dData)
|
||||
}
|
||||
}
|
||||
return dockerDataList, err
|
||||
}
|
||||
|
||||
func parseTasks(ctx context.Context, task swarmtypes.Task, serviceDockerData dockerData,
|
||||
networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) dockerData {
|
||||
dData := dockerData{
|
||||
ID: task.ID,
|
||||
ServiceName: serviceDockerData.Name,
|
||||
Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
|
||||
Labels: serviceDockerData.Labels,
|
||||
ExtraConf: serviceDockerData.ExtraConf,
|
||||
NetworkSettings: networkSettings{},
|
||||
}
|
||||
|
||||
if isGlobalSvc {
|
||||
dData.Name = serviceDockerData.Name + "." + task.ID
|
||||
}
|
||||
|
||||
if task.NetworksAttachments != nil {
|
||||
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||
for _, virtualIP := range task.NetworksAttachments {
|
||||
if networkService, present := networkMap[virtualIP.Network.ID]; present {
|
||||
if len(virtualIP.Addresses) > 0 {
|
||||
// Not sure about this next loop - when would a task have multiple IP's for the same network?
|
||||
for _, addr := range virtualIP.Addresses {
|
||||
ip, _, _ := net.ParseCIDR(addr)
|
||||
network := &networkData{
|
||||
ID: virtualIP.Network.ID,
|
||||
Name: networkService.Name,
|
||||
Addr: ip.String(),
|
||||
}
|
||||
dData.NetworkSettings.Networks[network.Name] = network
|
||||
}
|
||||
} else {
|
||||
log.FromContext(ctx).Debugf("No IP addresses found for network %s", virtualIP.Network.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dData
|
||||
}
|
8
pkg/provider/docker/docker_unix.go
Normal file
8
pkg/provider/docker/docker_unix.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
// +build !windows
|
||||
|
||||
package docker
|
||||
|
||||
const (
|
||||
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
|
||||
DockerAPIVersion = "1.21"
|
||||
)
|
6
pkg/provider/docker/docker_windows.go
Normal file
6
pkg/provider/docker/docker_windows.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package docker
|
||||
|
||||
const (
|
||||
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
|
||||
DockerAPIVersion string = "1.24"
|
||||
)
|
63
pkg/provider/docker/label.go
Normal file
63
pkg/provider/docker/label.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containous/traefik/pkg/provider/label"
|
||||
)
|
||||
|
||||
const (
|
||||
labelDockerComposeProject = "com.docker.compose.project"
|
||||
labelDockerComposeService = "com.docker.compose.service"
|
||||
)
|
||||
|
||||
// configuration Contains information from the labels that are globals (not related to the dynamic configuration) or specific to the provider.
|
||||
type configuration struct {
|
||||
Enable bool
|
||||
Tags []string
|
||||
Docker specificConfiguration
|
||||
}
|
||||
|
||||
type specificConfiguration struct {
|
||||
Network string
|
||||
LBSwarm bool
|
||||
}
|
||||
|
||||
func (p *Provider) getConfiguration(container dockerData) (configuration, error) {
|
||||
conf := configuration{
|
||||
Enable: p.ExposedByDefault,
|
||||
Docker: specificConfiguration{
|
||||
Network: p.Network,
|
||||
},
|
||||
}
|
||||
|
||||
err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.enable", "traefik.tags")
|
||||
if err != nil {
|
||||
return configuration{}, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// getStringMultipleStrict get multiple string values associated to several labels
|
||||
// Fail if one label is missing
|
||||
func getStringMultipleStrict(labels map[string]string, labelNames ...string) (map[string]string, error) {
|
||||
foundLabels := map[string]string{}
|
||||
for _, name := range labelNames {
|
||||
value := getStringValue(labels, name, "")
|
||||
// Error out only if one of them is not defined.
|
||||
if len(value) == 0 {
|
||||
return nil, fmt.Errorf("label not found: %s", name)
|
||||
}
|
||||
foundLabels[name] = value
|
||||
}
|
||||
return foundLabels, nil
|
||||
}
|
||||
|
||||
// getStringValue get string value associated to a label
|
||||
func getStringValue(labels map[string]string, labelName string, defaultValue string) string {
|
||||
if value, ok := labels[labelName]; ok && len(value) > 0 {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
412
pkg/provider/docker/swarm_test.go
Normal file
412
pkg/provider/docker/swarm_test.go
Normal file
|
@ -0,0 +1,412 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
docker "github.com/docker/docker/api/types"
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeTasksClient struct {
|
||||
dockerclient.APIClient
|
||||
tasks []swarm.Task
|
||||
container dockertypes.ContainerJSON
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
|
||||
return c.tasks, c.err
|
||||
}
|
||||
|
||||
func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) {
|
||||
return c.container, c.err
|
||||
}
|
||||
|
||||
func TestListTasks(t *testing.T) {
|
||||
testCases := []struct {
|
||||
service swarm.Service
|
||||
tasks []swarm.Task
|
||||
isGlobalSVC bool
|
||||
expectedTasks []string
|
||||
networks map[string]*docker.NetworkResource
|
||||
}{
|
||||
{
|
||||
service: swarmService(serviceName("container")),
|
||||
tasks: []swarm.Task{
|
||||
swarmTask("id1",
|
||||
taskSlot(1),
|
||||
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.1"}),
|
||||
taskStatus(taskState(swarm.TaskStateRunning)),
|
||||
),
|
||||
swarmTask("id2",
|
||||
taskSlot(2),
|
||||
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.2"}),
|
||||
taskStatus(taskState(swarm.TaskStatePending)),
|
||||
),
|
||||
swarmTask("id3",
|
||||
taskSlot(3),
|
||||
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.3"}),
|
||||
),
|
||||
swarmTask("id4",
|
||||
taskSlot(4),
|
||||
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.4"}),
|
||||
taskStatus(taskState(swarm.TaskStateRunning)),
|
||||
),
|
||||
swarmTask("id5",
|
||||
taskSlot(5),
|
||||
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.5"}),
|
||||
taskStatus(taskState(swarm.TaskStateFailed)),
|
||||
),
|
||||
},
|
||||
isGlobalSVC: false,
|
||||
expectedTasks: []string{
|
||||
"container.1",
|
||||
"container.4",
|
||||
},
|
||||
networks: map[string]*docker.NetworkResource{
|
||||
"1": {
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for caseID, test := range testCases {
|
||||
test := test
|
||||
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := Provider{}
|
||||
dockerData, err := p.parseService(context.Background(), test.service, test.networks)
|
||||
require.NoError(t, err)
|
||||
|
||||
dockerClient := &fakeTasksClient{tasks: test.tasks}
|
||||
taskDockerData, _ := listTasks(context.Background(), dockerClient, test.service.ID, dockerData, test.networks, test.isGlobalSVC)
|
||||
|
||||
if len(test.expectedTasks) != len(taskDockerData) {
|
||||
t.Errorf("expected tasks %v, got %v", spew.Sdump(test.expectedTasks), spew.Sdump(taskDockerData))
|
||||
}
|
||||
|
||||
for i, taskID := range test.expectedTasks {
|
||||
if taskDockerData[i].Name != taskID {
|
||||
t.Errorf("expect task id %v, got %v", taskID, taskDockerData[i].Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeServicesClient struct {
|
||||
dockerclient.APIClient
|
||||
dockerVersion string
|
||||
networks []dockertypes.NetworkResource
|
||||
services []swarm.Service
|
||||
tasks []swarm.Task
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeServicesClient) ServiceList(ctx context.Context, options dockertypes.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return c.services, c.err
|
||||
}
|
||||
|
||||
func (c *fakeServicesClient) ServerVersion(ctx context.Context) (dockertypes.Version, error) {
|
||||
return dockertypes.Version{APIVersion: c.dockerVersion}, c.err
|
||||
}
|
||||
|
||||
func (c *fakeServicesClient) NetworkList(ctx context.Context, options dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) {
|
||||
return c.networks, c.err
|
||||
}
|
||||
|
||||
func (c *fakeServicesClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
|
||||
return c.tasks, c.err
|
||||
}
|
||||
|
||||
func TestListServices(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
services []swarm.Service
|
||||
tasks []swarm.Task
|
||||
dockerVersion string
|
||||
networks []dockertypes.NetworkResource
|
||||
expectedServices []string
|
||||
}{
|
||||
{
|
||||
desc: "Should return no service due to no networks defined",
|
||||
services: []swarm.Service{
|
||||
swarmService(
|
||||
serviceName("service1"),
|
||||
serviceLabels(map[string]string{
|
||||
"traefik.docker.network": "barnet",
|
||||
"traefik.docker.LBSwarm": "true",
|
||||
}),
|
||||
withEndpointSpec(modeVIP),
|
||||
withEndpoint(
|
||||
virtualIP("1", "10.11.12.13/24"),
|
||||
virtualIP("2", "10.11.12.99/24"),
|
||||
)),
|
||||
swarmService(
|
||||
serviceName("service2"),
|
||||
serviceLabels(map[string]string{
|
||||
"traefik.docker.network": "barnet",
|
||||
"traefik.docker.LBSwarm": "true",
|
||||
}),
|
||||
withEndpointSpec(modeDNSSR)),
|
||||
},
|
||||
dockerVersion: "1.30",
|
||||
networks: []dockertypes.NetworkResource{},
|
||||
expectedServices: []string{},
|
||||
},
|
||||
{
|
||||
desc: "Should return only service1",
|
||||
services: []swarm.Service{
|
||||
swarmService(
|
||||
serviceName("service1"),
|
||||
serviceLabels(map[string]string{
|
||||
"traefik.docker.network": "barnet",
|
||||
"traefik.docker.LBSwarm": "true",
|
||||
}),
|
||||
withEndpointSpec(modeVIP),
|
||||
withEndpoint(
|
||||
virtualIP("yk6l57rfwizjzxxzftn4amaot", "10.11.12.13/24"),
|
||||
virtualIP("2", "10.11.12.99/24"),
|
||||
)),
|
||||
swarmService(
|
||||
serviceName("service2"),
|
||||
serviceLabels(map[string]string{
|
||||
"traefik.docker.network": "barnet",
|
||||
"traefik.docker.LBSwarm": "true",
|
||||
}),
|
||||
withEndpointSpec(modeDNSSR)),
|
||||
},
|
||||
dockerVersion: "1.30",
|
||||
networks: []dockertypes.NetworkResource{
|
||||
{
|
||||
Name: "network_name",
|
||||
ID: "yk6l57rfwizjzxxzftn4amaot",
|
||||
Created: time.Now(),
|
||||
Scope: "swarm",
|
||||
Driver: "overlay",
|
||||
EnableIPv6: false,
|
||||
Internal: true,
|
||||
Ingress: false,
|
||||
ConfigOnly: false,
|
||||
Options: map[string]string{
|
||||
"com.docker.network.driver.overlay.vxlanid_list": "4098",
|
||||
"com.docker.network.enable_ipv6": "false",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"com.docker.stack.namespace": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedServices: []string{
|
||||
"service1",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Should return service1 and service2",
|
||||
services: []swarm.Service{
|
||||
swarmService(
|
||||
serviceName("service1"),
|
||||
serviceLabels(map[string]string{
|
||||
"traefik.docker.network": "barnet",
|
||||
}),
|
||||
withEndpointSpec(modeVIP),
|
||||
withEndpoint(
|
||||
virtualIP("yk6l57rfwizjzxxzftn4amaot", "10.11.12.13/24"),
|
||||
virtualIP("2", "10.11.12.99/24"),
|
||||
)),
|
||||
swarmService(
|
||||
serviceName("service2"),
|
||||
serviceLabels(map[string]string{
|
||||
"traefik.docker.network": "barnet",
|
||||
}),
|
||||
withEndpointSpec(modeDNSSR)),
|
||||
},
|
||||
tasks: []swarm.Task{
|
||||
swarmTask("id1",
|
||||
taskNetworkAttachment("yk6l57rfwizjzxxzftn4amaot", "network_name", "overlay", []string{"127.0.0.1"}),
|
||||
taskStatus(taskState(swarm.TaskStateRunning)),
|
||||
),
|
||||
swarmTask("id2",
|
||||
taskNetworkAttachment("yk6l57rfwizjzxxzftn4amaot", "network_name", "overlay", []string{"127.0.0.1"}),
|
||||
taskStatus(taskState(swarm.TaskStateRunning)),
|
||||
),
|
||||
},
|
||||
dockerVersion: "1.30",
|
||||
networks: []dockertypes.NetworkResource{
|
||||
{
|
||||
Name: "network_name",
|
||||
ID: "yk6l57rfwizjzxxzftn4amaot",
|
||||
Created: time.Now(),
|
||||
Scope: "swarm",
|
||||
Driver: "overlay",
|
||||
EnableIPv6: false,
|
||||
Internal: true,
|
||||
Ingress: false,
|
||||
ConfigOnly: false,
|
||||
Options: map[string]string{
|
||||
"com.docker.network.driver.overlay.vxlanid_list": "4098",
|
||||
"com.docker.network.enable_ipv6": "false",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"com.docker.stack.namespace": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedServices: []string{
|
||||
"service1.0",
|
||||
"service1.0",
|
||||
"service2.0",
|
||||
"service2.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dockerClient := &fakeServicesClient{services: test.services, tasks: test.tasks, dockerVersion: test.dockerVersion, networks: test.networks}
|
||||
|
||||
p := Provider{}
|
||||
|
||||
serviceDockerData, err := p.listServices(context.Background(), dockerClient)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, len(test.expectedServices), len(serviceDockerData))
|
||||
for i, serviceName := range test.expectedServices {
|
||||
if len(serviceDockerData) <= i {
|
||||
require.Fail(t, "index", "invalid index %d", i)
|
||||
}
|
||||
assert.Equal(t, serviceName, serviceDockerData[i].Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwarmTaskParsing(t *testing.T) {
|
||||
testCases := []struct {
|
||||
service swarm.Service
|
||||
tasks []swarm.Task
|
||||
isGlobalSVC bool
|
||||
expected map[string]dockerData
|
||||
networks map[string]*docker.NetworkResource
|
||||
}{
|
||||
{
|
||||
service: swarmService(serviceName("container")),
|
||||
tasks: []swarm.Task{
|
||||
swarmTask("id1", taskSlot(1)),
|
||||
swarmTask("id2", taskSlot(2)),
|
||||
swarmTask("id3", taskSlot(3)),
|
||||
},
|
||||
isGlobalSVC: false,
|
||||
expected: map[string]dockerData{
|
||||
"id1": {
|
||||
Name: "container.1",
|
||||
},
|
||||
"id2": {
|
||||
Name: "container.2",
|
||||
},
|
||||
"id3": {
|
||||
Name: "container.3",
|
||||
},
|
||||
},
|
||||
networks: map[string]*docker.NetworkResource{
|
||||
"1": {
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
service: swarmService(serviceName("container")),
|
||||
tasks: []swarm.Task{
|
||||
swarmTask("id1"),
|
||||
swarmTask("id2"),
|
||||
swarmTask("id3"),
|
||||
},
|
||||
isGlobalSVC: true,
|
||||
expected: map[string]dockerData{
|
||||
"id1": {
|
||||
Name: "container.id1",
|
||||
},
|
||||
"id2": {
|
||||
Name: "container.id2",
|
||||
},
|
||||
"id3": {
|
||||
Name: "container.id3",
|
||||
},
|
||||
},
|
||||
networks: map[string]*docker.NetworkResource{
|
||||
"1": {
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
service: swarmService(
|
||||
serviceName("container"),
|
||||
withEndpointSpec(modeVIP),
|
||||
withEndpoint(
|
||||
virtualIP("1", ""),
|
||||
),
|
||||
),
|
||||
tasks: []swarm.Task{
|
||||
swarmTask(
|
||||
"id1",
|
||||
taskNetworkAttachment("1", "vlan", "macvlan", []string{"127.0.0.1"}),
|
||||
taskStatus(
|
||||
taskState(swarm.TaskStateRunning),
|
||||
taskContainerStatus("c1"),
|
||||
),
|
||||
),
|
||||
},
|
||||
isGlobalSVC: true,
|
||||
expected: map[string]dockerData{
|
||||
"id1": {
|
||||
Name: "container.id1",
|
||||
NetworkSettings: networkSettings{
|
||||
Networks: map[string]*networkData{
|
||||
"vlan": {
|
||||
Name: "vlan",
|
||||
Addr: "10.11.12.13",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
networks: map[string]*docker.NetworkResource{
|
||||
"1": {
|
||||
Name: "vlan",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for caseID, test := range testCases {
|
||||
test := test
|
||||
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := Provider{}
|
||||
|
||||
dData, err := p.parseService(context.Background(), test.service, test.networks)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, task := range test.tasks {
|
||||
taskDockerData := parseTasks(context.Background(), task, dData, test.networks, test.isGlobalSVC)
|
||||
expected := test.expected[task.ID]
|
||||
assert.Equal(t, expected.Name, taskDockerData.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue