diff --git a/commands.go b/commands.go index c802e8096..36826fc4b 100644 --- a/commands.go +++ b/commands.go @@ -1122,6 +1122,11 @@ func (cli *DockerCli) CmdLinks(args ...string) error { return err } + if cmd.NArg() < 1 && !*flAll { + cmd.Usage() + return nil + } + v := url.Values{} v.Set("name", cmd.Arg(0)) diff --git a/container.go b/container.go index 75119f656..c81a97afe 100644 --- a/container.go +++ b/container.go @@ -125,6 +125,14 @@ func (p Port) Port() string { return strings.Split(string(p), "/")[0] } +func (p Port) Int() int { + i, err := parsePort(p.Port()) + if err != nil { + panic(err) + } + return i +} + func NewPort(proto, port string) Port { return Port(fmt.Sprintf("%s/%s", port, proto)) } @@ -830,6 +838,9 @@ func (container *Container) Start(hostConfig *HostConfig) error { return err } linkedContainer := runtime.Get(parts["id"]) + if linkedContainer == nil { + return fmt.Errorf("Cannot find container: %s", parts["id"]) + } link, err := runtime.links.NewLink(container, linkedContainer, runtime.networkManager.bridgeIface, parts["alias"]) if err != nil { diff --git a/links.go b/links.go index cc66d4280..48ec2880f 100644 --- a/links.go +++ b/links.go @@ -26,6 +26,9 @@ type LinkRepository struct { } func (r *LinkRepository) NewLink(to, from *Container, bridgeInterface string, alias string) (*Link, error) { + if to.ID == from.ID { + return nil, fmt.Errorf("Cannot link to self: %s == %s", to.ID, from.ID) + } if !from.State.Running { return nil, fmt.Errorf("Cannot link to a non running container: %s AS %s", from.ID, alias) } @@ -39,7 +42,7 @@ func (r *LinkRepository) NewLink(to, from *Container, bridgeInterface string, al FromID: utils.TruncateID(from.ID), ToID: utils.TruncateID(to.ID), BridgeInterface: bridgeInterface, - Alias: alias, + Alias: strings.ToUpper(alias), FromIP: from.NetworkSettings.IPAddress, ToIP: to.NetworkSettings.IPAddress, FromEnvironment: from.Config.Env, @@ -57,25 +60,55 @@ func (l *Link) ID() string { func (l *Link) ToEnv() []string { env := []string{} + + if p := l.getDefaultPort(); p != nil { + env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", l.Alias, p.Proto(), l.FromIP, p.Port())) + } + // Load exposed ports into the environment for _, p := range l.ports { - env = append(env, fmt.Sprintf("%s_%s_ADDR=%s://%s:%s", strings.ToUpper(l.Alias), p.Port(), p.Proto(), l.FromIP, p.Port())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s=%s://%s:%s", l.Alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.FromIP, p.Port())) } + // Load the linked container's ID into the environment + env = append(env, fmt.Sprintf("%s_ID=%s", l.Alias, l.FromID)) + if l.FromEnvironment != nil { for _, v := range l.FromEnvironment { parts := strings.Split(v, "=") - if len(parts) < 2 { + if len(parts) != 2 { continue } - env = append(env, fmt.Sprintf("%s_ENV_%s=%s", strings.ToUpper(l.Alias), parts[0], parts[1])) + // Ignore a few variables that are added during docker build + if parts[0] == "HOME" || parts[0] == "PATH" { + continue + } + env = append(env, fmt.Sprintf("%s_ENV_%s=%s", l.Alias, parts[0], parts[1])) } } return env } +// Default port rules +func (l *Link) getDefaultPort() *Port { + var p Port + i := len(l.ports) + + if i == 0 { + return nil + } else if i > 1 { + sortPorts(l.ports, func(ip, jp 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() && ip.Proto() == "tcp") + }) + } + p = l.ports[0] + return &p +} + func (l *Link) Enable() error { - if err := l.toggle("-I"); err != nil { + if err := l.toggle("-I", false); err != nil { return err } l.isEnabled = true @@ -85,12 +118,12 @@ func (l *Link) Enable() error { func (l *Link) Disable() { // We do not care about erros here because the link may not // exist in iptables - l.toggle("-D") + l.toggle("-D", true) l.isEnabled = false } -func (l *Link) toggle(action string) error { +func (l *Link) toggle(action string, ignoreErrors bool) error { for _, p := range l.ports { if err := iptables.Raw(action, "FORWARD", "-i", l.BridgeInterface, "-o", l.BridgeInterface, @@ -98,7 +131,7 @@ func (l *Link) toggle(action string) error { "-s", l.ToIP, "--dport", p.Port(), "-d", l.FromIP, - "-j", "ACCEPT"); err != nil { + "-j", "ACCEPT"); !ignoreErrors && err != nil { return err } @@ -108,7 +141,7 @@ func (l *Link) toggle(action string) error { "-s", l.FromIP, "--sport", p.Port(), "-d", l.ToIP, - "-j", "ACCEPT"); err != nil { + "-j", "ACCEPT"); !ignoreErrors && err != nil { return err } } diff --git a/links_test.go b/links_test.go new file mode 100644 index 000000000..887ad865e --- /dev/null +++ b/links_test.go @@ -0,0 +1,122 @@ +package docker + +import ( + "fmt" + "github.com/dotcloud/docker/utils" + "strings" + "testing" +) + +func newTestLinkRepository(t *testing.T) *LinkRepository { + r, err := NewLinkRepository("") + if err != nil { + t.Fatal(err) + } + return r +} + +func newMockLinkContainer(id string, ip string) *Container { + return &Container{ + Config: &Config{}, + ID: id, + NetworkSettings: &NetworkSettings{ + IPAddress: ip, + }, + } +} + +func TestLinkNew(t *testing.T) { + r := newTestLinkRepository(t) + toID := GenerateID() + fromID := GenerateID() + + from := newMockLinkContainer(fromID, "172.0.17.2") + from.Config.Env = []string{} + from.State = State{Running: true} + ports := make(map[Port]struct{}) + + ports[Port("6379/tcp")] = struct{}{} + + from.Config.ExposedPorts = ports + + to := newMockLinkContainer(toID, "172.0.17.3") + + link, err := r.NewLink(to, from, "172.0.17.1", "docker") + if err != nil { + t.Fatal(err) + } + + if link == nil { + t.FailNow() + } + if link.ID() != fmt.Sprintf("%s:%s", utils.TruncateID(to.ID), "DOCKER") { + t.Fail() + } + if link.Alias != "DOCKER" { + t.Fail() + } + if link.FromID != utils.TruncateID(from.ID) { + t.Fail() + } + if link.ToID != utils.TruncateID(to.ID) { + t.Fail() + } + if link.ToIP != "172.0.17.3" { + t.Fail() + } + if link.FromIP != "172.0.17.2" { + t.Fail() + } + if link.BridgeInterface != "172.0.17.1" { + t.Fail() + } + for _, p := range link.ports { + if p != Port("6379/tcp") { + t.Fail() + } + } +} + +func TestLinkEnv(t *testing.T) { + r := newTestLinkRepository(t) + toID := GenerateID() + fromID := GenerateID() + + from := newMockLinkContainer(fromID, "172.0.17.2") + from.Config.Env = []string{"PASSWORD=gordon"} + from.State = State{Running: true} + ports := make(map[Port]struct{}) + + ports[Port("6379/tcp")] = struct{}{} + + from.Config.ExposedPorts = ports + + to := newMockLinkContainer(toID, "172.0.17.3") + + link, err := r.NewLink(to, from, "172.0.17.1", "docker") + if err != nil { + t.Fatal(err) + } + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" { + t.Fail() + } + if env["DOCKER_PORT_6379_TCP"] != "tcp://172.0.17.2:6379" { + t.Fail() + } + if env["DOCKER_ID"] != utils.TruncateID(from.ID) { + t.Fail() + } + if env["DOCKER_ENV_PASSWORD"] != "gordon" { + t.Fail() + } +} diff --git a/sorter.go b/sorter.go index a81884148..09f1fcbe0 100644 --- a/sorter.go +++ b/sorter.go @@ -34,3 +34,28 @@ func sortImagesByCreationAndTag(images []APIImages) { sort.Sort(sorter) } + +type portSorter struct { + ports []Port + by func(i, j Port) bool +} + +func (s *portSorter) Len() int { + return len(s.ports) +} + +func (s *portSorter) Swap(i, j int) { + s.ports[i], s.ports[j] = s.ports[j], s.ports[i] +} + +func (s *portSorter) Less(i, j int) bool { + ip := s.ports[i] + jp := s.ports[j] + + return s.by(ip, jp) +} + +func sortPorts(ports []Port, predicate func(i, j Port) bool) { + s := &portSorter{ports, predicate} + sort.Sort(s) +} diff --git a/sorter_test.go b/sorter_test.go index 5519708ec..d61b1a711 100644 --- a/sorter_test.go +++ b/sorter_test.go @@ -1,6 +1,7 @@ package docker import ( + "fmt" "testing" ) @@ -55,3 +56,38 @@ func TestServerListOrderedImagesByCreationDateAndTag(t *testing.T) { t.Error("Expected []APIImges to be ordered by most recent creation date and tag name.") } } + +func TestSortUniquePorts(t *testing.T) { + ports := []Port{ + Port("6379/tcp"), + Port("22/tcp"), + } + + sortPorts(ports, func(ip, jp Port) bool { + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp") + }) + + first := ports[0] + if fmt.Sprint(first) != "22/tcp" { + t.Log(fmt.Sprint(first)) + t.Fail() + } +} + +func TestSortSamePortWithDifferentProto(t *testing.T) { + ports := []Port{ + Port("8888/tcp"), + Port("8888/udp"), + Port("6379/tcp"), + Port("6379/udp"), + } + + sortPorts(ports, func(ip, jp Port) bool { + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp") + }) + + first := ports[0] + if fmt.Sprint(first) != "6379/tcp" { + t.Fail() + } +}