Skip to content

Commit

Permalink
Merge pull request moby#49300 from thaJeztah/links_fixes
Browse files Browse the repository at this point in the history
daemon/links: assorted bug fixes and cleanup
  • Loading branch information
thaJeztah authored Jan 20, 2025
2 parents 6160aeb + 76a496a commit cb4cc87
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 82 deletions.
10 changes: 4 additions & 6 deletions daemon/container_operations_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,13 @@ import (
)

func (daemon *Daemon) setupLinkedContainers(ctr *container.Container) ([]string, error) {
var env []string
children := daemon.children(ctr)

bridgeSettings := ctr.NetworkSettings.Networks[network.DefaultNetwork]
if bridgeSettings == nil || bridgeSettings.EndpointSettings == nil {
return nil, nil
}

for linkAlias, child := range children {
var env []string
for linkAlias, child := range daemon.children(ctr) {
if !child.IsRunning() {
return nil, fmt.Errorf("Cannot link to a non running container: %s AS %s", child.Name, linkAlias)
}
Expand All @@ -48,15 +46,15 @@ func (daemon *Daemon) setupLinkedContainers(ctr *container.Container) ([]string,
return nil, fmt.Errorf("container %s not attached to default bridge network", child.ID)
}

link := links.NewLink(
linkEnvVars := links.EnvVars(
bridgeSettings.IPAddress,
childBridgeSettings.IPAddress,
linkAlias,
child.Config.Env,
child.Config.ExposedPorts,
)

env = append(env, link.ToEnv()...)
env = append(env, linkEnvVars...)
}

return env, nil
Expand Down
123 changes: 55 additions & 68 deletions daemon/links/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ type Link struct {
Ports []nat.Port
}

// EnvVars generates environment variables for the linked container
// for the Link with the given options.
func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[nat.Port]struct{}) []string {
return NewLink(parentIP, childIP, name, env, exposedPorts).ToEnv()
}

// NewLink initializes a new Link struct with the provided options.
func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[nat.Port]struct{}) *Link {
var (
i int
ports = make([]nat.Port, len(exposedPorts))
)

ports := make([]nat.Port, 0, len(exposedPorts))
for p := range exposedPorts {
ports[i] = p
i++
ports = append(ports, p)
}

return &Link{
Expand All @@ -47,46 +48,47 @@ func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[nat.
// the form of environment variables which will be later exported on container
// startup.
func (l *Link) ToEnv() []string {
env := []string{}

_, n := path.Split(l.Name)
alias := strings.ReplaceAll(strings.ToUpper(n), "-", "_")

if p := l.getDefaultPort(); p != nil {
env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port()))
}

// sort the ports so that we can bulk the continuous ports together
nat.Sort(l.Ports, func(ip, jp nat.Port) bool {
// If the two ports have the same number, tcp takes priority
// Sort in desc order
return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp")
})

for i := 0; i < len(l.Ports); {
p := l.Ports[i]
j := nextContiguous(l.Ports, p.Int(), i)
if j > i+1 {
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_START=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port()))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto()))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_START=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port()))

q := l.Ports[j]
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_END=%s://%s:%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Proto(), l.ChildIP, q.Port()))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_END=%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Port()))

i = j + 1
continue
} else {
i++
nat.Sort(l.Ports, withTCPPriority)

var pStart, pEnd nat.Port
env := make([]string, 0, 1+len(l.Ports)*4)
for i, p := range l.Ports {
if i == 0 {
pStart, pEnd = p, p
env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port()))
}
}
for _, p := range l.Ports {
env = append(env, fmt.Sprintf("%s_PORT_%s_%s=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port()))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port()))
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto()))

// These env-vars are produced for every port, regardless if they're
// part of a port-range.
prefix := fmt.Sprintf("%s_PORT_%s_%s", alias, p.Port(), strings.ToUpper(p.Proto()))
env = append(env, fmt.Sprintf("%s=%s://%s:%s", prefix, p.Proto(), l.ChildIP, p.Port()))
env = append(env, fmt.Sprintf("%s_ADDR=%s", prefix, l.ChildIP))
env = append(env, fmt.Sprintf("%s_PORT=%s", prefix, p.Port()))
env = append(env, fmt.Sprintf("%s_PROTO=%s", prefix, p.Proto()))

// Detect whether this port is part of a range (consecutive port number
// and same protocol).
if p.Int() == pEnd.Int()+1 && strings.EqualFold(p.Proto(), pStart.Proto()) {
pEnd = p
if i < len(l.Ports)-1 {
continue
}
}

if pEnd != pStart {
prefix = fmt.Sprintf("%s_PORT_%s_%s", alias, pStart.Port(), strings.ToUpper(pStart.Proto()))
env = append(env, fmt.Sprintf("%s_START=%s://%s:%s", prefix, pStart.Proto(), l.ChildIP, pStart.Port()))
env = append(env, fmt.Sprintf("%s_PORT_START=%s", prefix, pStart.Port()))
env = append(env, fmt.Sprintf("%s_END=%s://%s:%s", prefix, pEnd.Proto(), l.ChildIP, pEnd.Port()))
env = append(env, fmt.Sprintf("%s_PORT_END=%s", prefix, pEnd.Port()))
}

// Reset for next range (if any)
pStart, pEnd = p, p
}

// Load the linked container's name into the environment
Expand All @@ -108,34 +110,19 @@ func (l *Link) ToEnv() []string {
return env
}

func nextContiguous(ports []nat.Port, value int, index int) int {
if index+1 == len(ports) {
return index
// withTCPPriority prioritizes ports using TCP over other protocols before
// comparing port-number and protocol.
func withTCPPriority(ip, jp nat.Port) bool {
if strings.EqualFold(ip.Proto(), jp.Proto()) {
return ip.Int() < jp.Int()
}
for i := index + 1; i < len(ports); i++ {
if ports[i].Int() > value+1 {
return i - 1
}

value++
if strings.EqualFold(ip.Proto(), "tcp") {
return true
}
return len(ports) - 1
}

// Default port rules
func (l *Link) getDefaultPort() *nat.Port {
var p nat.Port
i := len(l.Ports)

if i == 0 {
return nil
} else if i > 1 {
nat.Sort(l.Ports, func(ip, jp nat.Port) bool {
// If the two ports have the same number, tcp takes priority
// Sort in desc order
return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp")
})
if strings.EqualFold(jp.Proto(), "tcp") {
return false
}
p = l.Ports[0]
return &p

return strings.ToLower(ip.Proto()) < strings.ToLower(jp.Proto())
}
67 changes: 59 additions & 8 deletions daemon/links/links_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func TestLinkNaming(t *testing.T) {
link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, nat.PortSet{
actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, nat.PortSet{
"6379/tcp": struct{}{},
})

Expand All @@ -22,7 +22,6 @@ func TestLinkNaming(t *testing.T) {
"DOCKER_1_PORT_6379_TCP_PROTO=tcp",
}

actual := link.ToEnv()
sort.Strings(actual) // order of env-vars is not relevant
assert.DeepEqual(t, expectedEnv, actual)
}
Expand All @@ -43,7 +42,7 @@ func TestLinkNew(t *testing.T) {
}

func TestLinkEnv(t *testing.T) {
link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, nat.PortSet{
actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, nat.PortSet{
"6379/tcp": struct{}{},
})

Expand All @@ -57,31 +56,65 @@ func TestLinkEnv(t *testing.T) {
"DOCKER_PORT_6379_TCP_PROTO=tcp",
}

actual := link.ToEnv()
sort.Strings(actual) // order of env-vars is not relevant
assert.DeepEqual(t, expectedEnv, actual)
}

// TestSortPorts verifies that ports are sorted with TCP taking priority,
// and ports with the same protocol to be sorted by port.
func TestSortPorts(t *testing.T) {
ports := []nat.Port{
"6379/tcp",
"6376/udp",
"6380/tcp",
"6376/sctp",
"6381/tcp",
"6381/udp",
"6375/udp",
"6375/sctp",
}

expected := []nat.Port{
"6379/tcp",
"6380/tcp",
"6381/tcp",
"6375/sctp",
"6376/sctp",
"6375/udp",
"6376/udp",
"6381/udp",
}

nat.Sort(ports, withTCPPriority)
assert.DeepEqual(t, expected, ports)
}

func TestLinkMultipleEnv(t *testing.T) {
link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, nat.PortSet{
actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, nat.PortSet{
"6300/udp": struct{}{},
"6379/tcp": struct{}{},
"6380/tcp": struct{}{},
"6381/tcp": struct{}{},
"6382/udp": struct{}{},
})

expectedEnv := []string{
"DOCKER_ENV_PASSWORD=gordon",
"DOCKER_NAME=/db/docker",
"DOCKER_PORT=tcp://172.0.17.2:6379",

"DOCKER_PORT_6300_UDP=udp://172.0.17.2:6300",
"DOCKER_PORT_6300_UDP_ADDR=172.0.17.2",
"DOCKER_PORT_6300_UDP_PORT=6300",
"DOCKER_PORT_6300_UDP_PROTO=udp",

"DOCKER_PORT_6379_TCP=tcp://172.0.17.2:6379",
"DOCKER_PORT_6379_TCP_ADDR=172.0.17.2",
"DOCKER_PORT_6379_TCP_ADDR=172.0.17.2", // FIXME(thaJeztah): duplicate?
"DOCKER_PORT_6379_TCP_END=tcp://172.0.17.2:6381",
"DOCKER_PORT_6379_TCP_PORT=6379",
"DOCKER_PORT_6379_TCP_PORT_END=6381",
"DOCKER_PORT_6379_TCP_PORT_START=6379",
"DOCKER_PORT_6379_TCP_PROTO=tcp",
"DOCKER_PORT_6379_TCP_PROTO=tcp", // FIXME(thaJeztah): duplicate?
"DOCKER_PORT_6379_TCP_START=tcp://172.0.17.2:6379",

"DOCKER_PORT_6380_TCP=tcp://172.0.17.2:6380",
Expand All @@ -93,9 +126,27 @@ func TestLinkMultipleEnv(t *testing.T) {
"DOCKER_PORT_6381_TCP_ADDR=172.0.17.2",
"DOCKER_PORT_6381_TCP_PORT=6381",
"DOCKER_PORT_6381_TCP_PROTO=tcp",

"DOCKER_PORT_6382_UDP=udp://172.0.17.2:6382",
"DOCKER_PORT_6382_UDP_ADDR=172.0.17.2",
"DOCKER_PORT_6382_UDP_PORT=6382",
"DOCKER_PORT_6382_UDP_PROTO=udp",
}

actual := link.ToEnv()
sort.Strings(actual) // order of env-vars is not relevant
assert.DeepEqual(t, expectedEnv, actual)
}

func BenchmarkLinkMultipleEnv(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, nat.PortSet{
"6300/udp": struct{}{},
"6379/tcp": struct{}{},
"6380/tcp": struct{}{},
"6381/tcp": struct{}{},
"6382/udp": struct{}{},
})
}
}

0 comments on commit cb4cc87

Please sign in to comment.