Automatically register IPv6 container addresses for services

You need to pass a new flag, `-ipv6`, to registrator, in order to enable
this support, to prevent surprising accidents, but once you do, everything
Just Works with feature parity between IPv4 and IPv6 addresses.  You can
also manage the overrides on a per-address-family basis, if you want to go
super-fancy.  Also has documentation!
This commit is contained in:
Matt Palmer 2016-01-11 11:31:56 +11:00 committed by Anton Belyaev
parent 1a1a9ae684
commit d6f6d9e8e6
6 changed files with 191 additions and 16 deletions

View File

@ -229,24 +229,46 @@ func (b *Bridge) add(containerId string, quiet bool) {
isGroup := len(servicePorts) > 1
for _, port := range servicePorts {
service := b.newService(port, isGroup)
if service == nil {
services := b.newServices(port, isGroup, 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)
}
b.services[container.ID] = append(b.services[container.ID], service)
log.Println("added:", container.ID[:12], service.ID)
}
}
func (b *Bridge) newService(port ServicePort, isgroup bool) *Service {
func (b *Bridge) newServices(port ServicePort, isgroup, quiet bool) []Service {
services := make([]Service, 0)
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)
}
}
return services
}
func (b *Bridge) newService(port ServicePort, isgroup, quiet, ipv6 bool) *Service {
container := port.container
defaultName := strings.Split(path.Base(container.Config.Image), ":")[0]
@ -266,7 +288,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 != "" {
@ -276,13 +298,21 @@ 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 {
@ -290,6 +320,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 b.config.UseIpFromLabel != "" {
containerIp := container.Config.Labels[b.config.UseIpFromLabel]

View File

@ -23,6 +23,7 @@ type Config struct {
HostIp string
Internal bool
UseIpFromLabel string
IPv6 bool
ForceTags string
RefreshTtl int
RefreshInterval int

View File

@ -54,18 +54,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)
@ -75,9 +89,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
}
}
}
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
dockerapi "github.com/fsouza/go-dockerclient"
)
func TestEscapedComma(t *testing.T) {
@ -57,3 +58,100 @@ func TestEscapedComma(t *testing.T) {
assert.EqualValues(t, c.Expected, results)
}
}
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" })
}

View File

@ -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_<port>_<key>`) or the global key
(`SERVICE_<key>`). The order of precedence, from highest to lowest, is:
1. `SERVICE_<port>_<key>_(IPV4|IPV6)`
1. `SERVICE_<port>_<key>`
1. `SERVICE_<key>_(IPV4|IPV6)`
1. `SERVICE_<key>`
## Examples
### Single service with defaults

View File

@ -21,6 +21,7 @@ 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 useIpFromLabel = flag.String("useIpFromLabel", "", "Use IP which is stored in a label assigned to the container")
var ipv6 = flag.Bool("ipv6", false, "Register services with container IPv6 addresses, if available")
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")
@ -100,6 +101,7 @@ func main() {
HostIp: *hostIp,
Internal: *internal,
UseIpFromLabel: *useIpFromLabel,
IPv6: *ipv6,
ForceTags: *forceTags,
RefreshTtl: *refreshTtl,
RefreshInterval: *refreshInterval,