blob: d36148b47f78a1e713d2ea6ce38ff9559ac4bdbc [file] [edit]
package links
import (
"cmp"
"fmt"
"maps"
"path"
"slices"
"strings"
"github.com/moby/moby/api/types/network"
)
// Link struct holds information about parent/child linked container
type Link struct {
// Parent container IP address
ParentIP string
// Child container IP address
ChildIP string
// Link name
Name string
// Child environments variables
ChildEnvironment []string
// Child exposed ports
Ports []network.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[network.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[network.Port]struct{}) *Link {
ports := slices.Collect(maps.Keys(exposedPorts))
return &Link{
Name: name,
ChildIP: childIP,
ParentIP: parentIP,
ChildEnvironment: env,
Ports: ports,
}
}
// ToEnv creates a string's slice containing child container information in
// the form of environment variables which will be later exported on container
// startup.
func (l *Link) ToEnv() []string {
_, n := path.Split(l.Name)
alias := strings.ReplaceAll(strings.ToUpper(n), "-", "_")
// sort the ports so that we can bulk the continuous ports together
slices.SortFunc(l.Ports, withTCPPriority)
env := make([]string, 0, 1+len(l.Ports)*4)
var pStart, pEnd network.Port
for i, p := range l.Ports {
if i == 0 {
pStart, pEnd = p, p
env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%d", alias, p.Proto(), l.ChildIP, p.Num()))
}
// These env-vars are produced for every port, regardless if they're part of a port-range.
prefix := fmt.Sprintf("%s_PORT_%d_%s", alias, p.Num(), strings.ToUpper(string(p.Proto())))
env = append(env, fmt.Sprintf("%s=%s://%s:%d", prefix, p.Proto(), l.ChildIP, p.Num()))
env = append(env, fmt.Sprintf("%s_ADDR=%s", prefix, l.ChildIP))
env = append(env, fmt.Sprintf("%s_PORT=%d", prefix, p.Num()))
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.Num() == pEnd.Num()+1 && p.Proto() == pEnd.Proto() {
pEnd = p
if i < len(l.Ports)-1 {
continue
}
}
if pEnd != pStart {
prefix = fmt.Sprintf("%s_PORT_%d_%s", alias, pStart.Num(), strings.ToUpper(string(pStart.Proto())))
env = append(env, fmt.Sprintf("%s_START=%s://%s:%d", prefix, pStart.Proto(), l.ChildIP, pStart.Num()))
env = append(env, fmt.Sprintf("%s_PORT_START=%d", prefix, pStart.Num()))
env = append(env, fmt.Sprintf("%s_END=%s://%s:%d", prefix, pEnd.Proto(), l.ChildIP, pEnd.Num()))
env = append(env, fmt.Sprintf("%s_PORT_END=%d", prefix, pEnd.Num()))
}
// Reset for next range (if any)
pStart, pEnd = p, p
}
// Load the linked container's name into the environment
env = append(env, fmt.Sprintf("%s_NAME=%s", alias, l.Name))
if l.ChildEnvironment != nil {
for _, v := range l.ChildEnvironment {
name, val, ok := strings.Cut(v, "=")
if !ok {
continue
}
// Ignore a few variables that are added during docker build (and not really relevant to linked containers)
if name == "HOME" || name == "PATH" {
continue
}
env = append(env, fmt.Sprintf("%s_ENV_%s=%s", alias, name, val))
}
}
return env
}
// withTCPPriority prioritizes ports using TCP over other protocols before
// comparing port-number and protocol.
func withTCPPriority(ip, jp network.Port) int {
if ip.Proto() == jp.Proto() {
return cmp.Compare(ip.Num(), jp.Num())
}
if ip.Proto() == network.TCP {
return -1
}
if jp.Proto() == network.TCP {
return 1
}
return cmp.Compare(ip.Proto(), jp.Proto())
}