diff --git a/bridge/bridge.go b/bridge/bridge.go index 673605df6..323640777 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -200,24 +200,48 @@ func (b *Bridge) add(containerId string, quiet bool) { } continue } - service := b.newService(port, len(ports) > 1) - if service == nil { + services := b.newServices(port, len(ports) > 1, quiet) + if len(services) == 0 { if !quiet { log.Println("ignored:", container.ID[:12], "service on port", port.ExposedPort) } continue } - err := b.registry.Register(service) - if err != nil { - log.Println("register failed:", service, err) - continue + for _, service := range services { + err := b.registry.Register(&service) + if err != nil { + log.Println("register failed:", service, err) + continue + } + b.services[container.ID] = append(b.services[container.ID], &service) + log.Println("added:", container.ID[:12], service.ID) + } + } +} + +func (b *Bridge) newServices(port ServicePort, isgroup, quiet bool) []Service { + services := make([]Service, 0) + + if b.config.IPv4 { + svc := b.newService(port, isgroup, quiet, false) + + if svc != nil { + services = append(services, *svc) + } + } + + if b.config.IPv6 && port.container.NetworkSettings.GlobalIPv6Address != "" { + svc := b.newService(port, isgroup, quiet, true) + + if svc != nil { + services = append(services, *svc) } - b.services[container.ID] = append(b.services[container.ID], service) - log.Println("added:", container.ID[:12], service.ID) } + + return services } -func (b *Bridge) newService(port ServicePort, isgroup bool) *Service { +func (b *Bridge) newService(port ServicePort, isgroup, quiet, ipv6 bool) *Service { container := port.container defaultName := strings.Split(path.Base(container.Config.Image), ":")[0] @@ -237,7 +261,7 @@ func (b *Bridge) newService(port ServicePort, isgroup bool) *Service { port.HostIP = b.config.HostIp } - metadata, metadataFromPort := serviceMetaData(container.Config, port.ExposedPort) + metadata, metadataFromPort := serviceMetaData(container.Config, port.ExposedPort, ipv6) ignore := mapDefault(metadata, "ignore", "") if ignore != "" { @@ -247,12 +271,20 @@ func (b *Bridge) newService(port ServicePort, isgroup bool) *Service { service := new(Service) service.Origin = port service.ID = hostname + ":" + container.Name[1:] + ":" + port.ExposedPort + if ipv6 { + service.ID += ":ipv6" + } service.Name = mapDefault(metadata, "name", defaultName) if isgroup && !metadataFromPort["name"] { service.Name += "-" + port.ExposedPort } var p int - if b.config.Internal == true { + // We *always* expose the container's address and port for IPv6. It's + // pointless having an address that supports end-to-end if we ignore it. + if ipv6 { + service.IP = port.container.NetworkSettings.GlobalIPv6Address + p, _ = strconv.Atoi(port.ExposedPort) + } else if b.config.Internal == true { service.IP = port.ExposedIP p, _ = strconv.Atoi(port.ExposedPort) } else { @@ -260,6 +292,9 @@ func (b *Bridge) newService(port ServicePort, isgroup bool) *Service { p, _ = strconv.Atoi(port.HostPort) } service.Port = p + if !quiet { + log.Println("service:", service.ID, "is named", service.Name, "and is on", service.IP, "port", service.Port) + } if port.PortType == "udp" { service.Tags = combineTags( diff --git a/bridge/types.go b/bridge/types.go index b1611127e..a048348ce 100644 --- a/bridge/types.go +++ b/bridge/types.go @@ -22,6 +22,8 @@ type RegistryAdapter interface { type Config struct { HostIp string Internal bool + IPv4 bool + IPv6 bool ForceTags string RefreshTtl int RefreshInterval int diff --git a/bridge/util.go b/bridge/util.go index 3b450faff..2e98df4b0 100644 --- a/bridge/util.go +++ b/bridge/util.go @@ -30,18 +30,32 @@ func combineTags(tagParts ...string) []string { return tags } -func serviceMetaData(config *dockerapi.Config, port string) (map[string]string, map[string]bool) { +func serviceMetaData(config *dockerapi.Config, port string, ipv6 bool) (map[string]string, map[string]bool) { meta := config.Env for k, v := range config.Labels { meta = append(meta, k+"="+v) } - metadata := make(map[string]string) - metadataFromPort := make(map[string]bool) + metadata := make(map[string]string) + metadataFromPort := make(map[string]bool) + metadataFromAF := make(map[string]bool) + metadataFromPortAF := make(map[string]bool) for _, kv := range meta { kvp := strings.SplitN(kv, "=", 2) + if (strings.HasSuffix(kvp[0], "_IPV4") && ipv6) || (strings.HasSuffix(kvp[0], "_IPV6") && !ipv6) { + // We can definitely ignore any key with the wrong address family suffix + continue + } if strings.HasPrefix(kvp[0], "SERVICE_") && len(kvp) > 1 { + af := false + if strings.HasSuffix(kvp[0], "_IPV4") { + af = true + kvp[0] = strings.TrimSuffix(kvp[0], "_IPV4") + } else if strings.HasSuffix(kvp[0], "_IPV6") { + af = true + kvp[0] = strings.TrimSuffix(kvp[0], "_IPV6") + } key := strings.ToLower(strings.TrimPrefix(kvp[0], "SERVICE_")) - if metadataFromPort[key] { + if metadataFromPortAF[key] || (!af && metadataFromPort[key]) { continue } portkey := strings.SplitN(key, "_", 2) @@ -51,9 +65,16 @@ func serviceMetaData(config *dockerapi.Config, port string) (map[string]string, continue } metadata[portkey[1]] = kvp[1] - metadataFromPort[portkey[1]] = true + if af { + metadataFromPortAF[portkey[1]] = true + } else { + metadataFromPort[portkey[1]] = true + } } else { metadata[key] = kvp[1] + if af { + metadataFromAF[key] = true + } } } } diff --git a/bridge/util_test.go b/bridge/util_test.go new file mode 100644 index 000000000..1ebd1f6e1 --- /dev/null +++ b/bridge/util_test.go @@ -0,0 +1,105 @@ +package bridge + +import ( + "testing" + + "github.com/stretchr/testify/assert" + dockerapi "github.com/fsouza/go-dockerclient" +) + +func containerMetaDataGlobalOnly() *dockerapi.Config { + cfg := new(dockerapi.Config) + cfg.Env = []string { + "SERVICE_NAME=something", + } + return cfg +} + +func containerMetaDataGlobalAF() *dockerapi.Config { + cfg := new(dockerapi.Config) + cfg.Env = []string { + "SERVICE_NAME=something", + "SERVICE_NAME_IPV4=lollerskates", + "SERVICE_NAME_IPV6=loll:ersk:ates", + } + return cfg +} + +func containerMetaDataPort80() *dockerapi.Config { + cfg := new(dockerapi.Config) + cfg.Env = []string { + "SERVICE_NAME=something", + "SERVICE_NAME_IPV6=loll:ersk:ates", + "SERVICE_80_NAME=somethingelse", + } + return cfg +} + +func containerMetaDataPort80AF() *dockerapi.Config { + cfg := new(dockerapi.Config) + cfg.Env = []string { + "SERVICE_NAME=something", + "SERVICE_NAME_IPV4=ermahgerd", + "SERVICE_NAME_IPV4=ermahgerd6", + "SERVICE_80_NAME=somethingelse", + "SERVICE_80_NAME_IPV4=lollerskates", + "SERVICE_80_NAME_IPV6=loll:ersk:ates", + } + return cfg +} + +func TestServiceMetaDataCustomName(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataGlobalOnly(), "80", false) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "something" }) +} + +func TestServiceMetaDataCustomPortName(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataPort80(), "80", false) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "somethingelse" }) +} + +func TestServiceMetaDataDifferentPort(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataPort80(), "443", false) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "something" }) +} + +func TestServiceMetaDataDifferentPortIPv6(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataPort80(), "443", true) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "loll:ersk:ates" }) +} + +func TestServiceMetaDataIPv4Override(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataGlobalAF(), "80", false) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "lollerskates" }) +} + +func TestServiceMetaDataIPv6Override(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataGlobalAF(), "80", true) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "loll:ersk:ates" }) +} + +func TestServiceMetaDataCustomPortIPv4Override(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataPort80AF(), "80", false) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "lollerskates" }) +} + +func TestServiceMetaDataCustomPortIPv6Override(t *testing.T) { + metadata, _ := serviceMetaData(containerMetaDataPort80AF(), "80", true) + + assert.NotNil(t, metadata) + assert.Equal(t, metadata, map[string]string { "name": "loll:ersk:ates" }) +} diff --git a/docs/user/services.md b/docs/user/services.md index d7a187c1e..1b55b9e1d 100644 --- a/docs/user/services.md +++ b/docs/user/services.md @@ -129,6 +129,26 @@ differentiate from a TCP service that could be listening on the same port. Although this can be overridden on containers with `SERVICE_ID` or `SERVICE_x_ID`, it is not recommended. +## IPv6 Support + +If you have Docker running with IPv6 enabled, and you pass the `-ipv6` flag +to Registrator, then Registrator will register services with the IPv6 +address of the container. + +All the same container override parameters are available to control naming, +tagging, etc of your services, and they apply by default to both the IPv4 +and IPv6 versions of the service. If you wish to apply an override to only +the IPv4 or IPv6 version of the service, you may suffix your environment +variable or label with `_IPV6` or `_IPV4`, as appropriate, to either the +port-specific key (`SERVICE__`) or the global key +(`SERVICE_`). The order of precedence, from highest to lowest, is: + +1. `SERVICE___(IPV4|IPV6)` +1. `SERVICE__` +1. `SERVICE__(IPV4|IPV6)` +1. `SERVICE_` + + ## Examples ### Single service with defaults diff --git a/registrator.go b/registrator.go index b76dc9441..f2d5f0c82 100644 --- a/registrator.go +++ b/registrator.go @@ -20,6 +20,8 @@ var versionChecker = usage.NewChecker("registrator", Version) var hostIp = flag.String("ip", "", "IP for ports mapped to the host") var internal = flag.Bool("internal", false, "Use internal ports instead of published ones") +var ipv6 = flag.Bool("ipv6", false, "Register services with container IPv6 addresses, if available") +var ipv4 = flag.Bool("ipv4", true, "Register services with IPv4 addresses") var refreshInterval = flag.Int("ttl-refresh", 0, "Frequency with which service TTLs are refreshed") var refreshTtl = flag.Int("ttl", 0, "TTL for services (default is no expiry)") var forceTags = flag.String("tags", "", "Append tags for all registered services") @@ -98,6 +100,8 @@ func main() { b, err := bridge.New(docker, flag.Arg(0), bridge.Config{ HostIp: *hostIp, Internal: *internal, + IPv4: *ipv4, + IPv6: *ipv6, ForceTags: *forceTags, RefreshTtl: *refreshTtl, RefreshInterval: *refreshInterval,