diff --git a/api.go b/api.go index ecce8ee7f..9a3c89f51 100644 --- a/api.go +++ b/api.go @@ -960,10 +960,42 @@ func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { func getLinksJSON(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { out := []APILink{} name := r.FormValue("name") + rawRm := r.FormValue("rm") - links := srv.runtime.links.Get(name) + rm, err := getBoolParam(rawRm) + if err != nil { + return err + } + + if rm { + link := srv.runtime.links.GetById(name) + if link != nil { + if err := srv.runtime.links.removeLink(link); err != nil { + return err + } + w.WriteHeader(http.StatusOK) + return nil + } + w.WriteHeader(http.StatusNotFound) + return nil + } + if name == "" { + return fmt.Errorf("Name cannot be empty for link") + } + + container := srv.runtime.Get(name) + if container == nil { + return fmt.Errorf("Container not found %s", name) + } + links := srv.runtime.links.Get(container) for _, l := range links { - out = append(out, APILink{l.To, l.From, l.IP, fmt.Sprint(l.Port), l.Alias}) + out = append(out, APILink{ + ID: l.ID(), + To: l.ToID, + From: l.FromID, + Port: fmt.Sprint(l.Port), + Alias: l.Alias, + }) } w.Header().Add("Content-Type", "application/json") diff --git a/api_params.go b/api_params.go index ecf3fac8b..fe6eaa9fd 100644 --- a/api_params.go +++ b/api_params.go @@ -122,9 +122,9 @@ type APICopy struct { } type APILink struct { + ID string To string From string - IP string Port string Alias string } diff --git a/commands.go b/commands.go index b1204fcd3..31a5641ba 100644 --- a/commands.go +++ b/commands.go @@ -1114,6 +1114,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { func (cli *DockerCli) CmdLink(args ...string) error { cmd := Subcmd("link", "[OPTIONS] CONTAINER", "Get the links for a container") + flRm := cmd.Bool("rm", false, "Remove an existing link by the link ID") if err := cmd.Parse(args); err != nil { return err @@ -1122,21 +1123,30 @@ func (cli *DockerCli) CmdLink(args ...string) error { v := url.Values{} v.Set("name", cmd.Arg(0)) - body, _, err := cli.call("GET", "/links/json?"+v.Encode(), nil) + if *flRm { + v.Set("rm", "1") + } + + body, statusCode, err := cli.call("GET", "/links/json?"+v.Encode(), nil) if err != nil { return err } + if *flRm && statusCode == 200 { + fmt.Printf("Link successfully removed: %s\n", cmd.Arg(0)) + return nil + } + var links []APILink if err := json.Unmarshal(body, &links); err != nil { return err } w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "FROM\tTO\tADDRESS\tALIAS") + fmt.Fprintf(w, "ID\tFROM\tTO\tPORT\tALIAS") fmt.Fprintf(w, "\n") for _, l := range links { - fmt.Fprintf(w, "%s\t%s\t%s\t%s", l.From, l.To, fmt.Sprintf("%s:%s", l.IP, l.Port), l.Alias) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s", l.ID, l.From, l.To, l.Port, l.Alias) fmt.Fprintf(w, "\n") } w.Flush() diff --git a/container.go b/container.go index fb68ca7b9..7843df654 100644 --- a/container.go +++ b/container.go @@ -91,7 +91,7 @@ type HostConfig struct { ContainerIDFile string LxcConf []KeyValuePair PortBindings map[Port][]PortBinding - Links []Link + Links []string } type BindMap struct { @@ -265,11 +265,6 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, } } - links, err := parseLinks(flLinks) - if err != nil { - return nil, nil, cmd, err - } - config := &Config{ Hostname: *flHostname, Domainname: domainname, @@ -300,7 +295,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, ContainerIDFile: *flContainerIDFile, LxcConf: lxcConf, PortBindings: portBindings, - Links: links, + Links: flLinks, } if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit { @@ -827,21 +822,31 @@ func (container *Container) Start(hostConfig *HostConfig) error { "-e", "HOSTNAME="+container.Config.Hostname, ) - if hostConfig != nil && hostConfig.Links != nil { + if !container.Config.NetworkDisabled && hostConfig != nil && hostConfig.Links != nil { runtime := container.runtime for _, l := range hostConfig.Links { - linkedContainer := runtime.Get(l.From) + parts, err := parseLink(l) + if err != nil { + return err + } + p := NewPort(splitProtoPort(parts["port"])) + linkedContainer := runtime.Get(parts["id"]) - if linkedContainer == nil { - return fmt.Errorf("Cannot locate container for link: %s AS %s", l.From, l.Alias) - } - if err := linkedContainer.AcceptLink(l); err != nil { + link, err := runtime.links.NewLink(container, linkedContainer, runtime.networkManager.bridgeIface, p, parts["alias"]) + if err != nil { return err } - if err := container.Link(linkedContainer, &l); err != nil { + + if err := link.Enable(); err != nil { + // If we encounter an err, make sure we remove all links + for _, registeredLinks := range runtime.links.Get(container) { + runtime.links.removeLink(registeredLinks) + } return err } - params = append(params, "-e", l.ToEnv()) + for _, envVar := range link.ToEnv() { + params = append(params, "-e", envVar) + } } } @@ -1080,6 +1085,13 @@ func (container *Container) monitor(hostConfig *HostConfig) { // Cleanup container.releaseNetwork() + + //Destroy all links + runtime := container.runtime + for _, link := range runtime.links.Get(container) { + runtime.links.removeLink(link) + } + if container.Config.OpenStdin { if err := container.stdin.Close(); err != nil { utils.Debugf("%s: Error close stdin: %s", container.ID, err) @@ -1370,26 +1382,6 @@ func (container *Container) Copy(resource string) (Archive, error) { return TarFilter(basePath, Uncompressed, filter) } -func (container *Container) AcceptLink(l Link) error { - if !container.State.Running { - return fmt.Errorf("Cannot accept link on a non running container: %s AS %s", l.From, l.Alias) - } - if !container.Exposes(l.Port) { - return fmt.Errorf("Cannot accept link to %s because %s is not exposed", container.ID, l.Port) - } - return nil -} - -func (container *Container) Link(c *Container, l *Link) error { - l.To = utils.TruncateID(container.ID) - l.IP = c.NetworkSettings.IPAddress - - if err := container.runtime.links.RegisterLink(*l); err != nil { - return err - } - return nil -} - // Returns true if the container exposes a certain port func (container *Container) Exposes(p Port) bool { _, exists := container.Config.ExposedPorts[p] diff --git a/links.go b/links.go index 40812e6d1..12f5b9cef 100644 --- a/links.go +++ b/links.go @@ -2,58 +2,147 @@ package docker import ( "fmt" + "github.com/dotcloud/docker/iptables" "github.com/dotcloud/docker/utils" "strings" ) +// A Link represents a connection between two containers +// for a specific port on a specific bridge interface type Link struct { - From string - To string - IP string - Port Port - Alias string + FromID string + ToID string + FromIP string + ToIP string + BridgeInterface string + Port Port + Alias string + FromEnvironment []string + isEnabled bool } type LinkRepository struct { - links map[string]Link + links map[string]*Link } -func (l *Link) ToEnv() string { - return fmt.Sprintf("%s_ADDR=%s://%s:%s", strings.ToUpper(l.Alias), l.Port.Proto(), l.IP, l.Port.Port()) +func (r *LinkRepository) NewLink(to, from *Container, bridgeInterface string, p Port, alias string) (*Link, error) { + if !from.State.Running { + return nil, fmt.Errorf("Cannot link to a non running container: %s AS %s", from.ID, alias) + } + if !from.Exposes(p) { + return nil, fmt.Errorf("Cannot link to %s because %s is not exposed", from.ID, p) + } + l := &Link{ + FromID: utils.TruncateID(from.ID), + ToID: utils.TruncateID(to.ID), + BridgeInterface: bridgeInterface, + Alias: alias, + Port: p, + FromIP: from.NetworkSettings.IPAddress, + ToIP: to.NetworkSettings.IPAddress, + FromEnvironment: from.Config.Env, + } + if err := r.registerLink(l); err != nil { + return nil, err + } + return l, nil +} + +func (l *Link) ID() string { + return fmt.Sprintf("%s:%s", l.ToID, l.Alias) +} + +func (l *Link) ToEnv() []string { + env := []string{fmt.Sprintf("%s_ADDR=%s://%s:%s", strings.ToUpper(l.Alias), l.Port.Proto(), l.FromIP, l.Port.Port())} + if l.FromEnvironment != nil { + for _, v := range l.FromEnvironment { + parts := strings.Split(v, "=") + if len(parts) < 2 { + continue + } + env = append(env, fmt.Sprintf("%s_ENV_%s=%s", strings.ToUpper(l.Alias), parts[0], parts[1])) + } + } + return env +} + +func (l *Link) Enable() error { + if err := l.toggle("-I"); err != nil { + return err + } + l.isEnabled = true + return nil +} + +func (l *Link) Disable() { + // We do not care about erros here because the link may not + // exist in iptables + l.toggle("-D") + + l.isEnabled = false +} + +func (l *Link) toggle(action string) error { + if err := iptables.Raw(action, "FORWARD", + "-i", l.BridgeInterface, "-o", l.BridgeInterface, + "-p", l.Port.Proto(), + "-s", l.ToIP, + "--dport", l.Port.Port(), + "-d", l.FromIP, + "-j", "ACCEPT"); err != nil { + return err + } + + if err := iptables.Raw(action, "FORWARD", + "-i", l.BridgeInterface, "-o", l.BridgeInterface, + "-p", l.Port.Proto(), + "-s", l.FromIP, + "--sport", l.Port.Port(), + "-d", l.ToIP, + "-j", "ACCEPT"); err != nil { + return err + } + return nil } func NewLinkRepository(root string) (*LinkRepository, error) { - r := &LinkRepository{make(map[string]Link)} + r := &LinkRepository{make(map[string]*Link)} return r, nil } // Return all links for a container -func (l *LinkRepository) Get(id string) []Link { - id = strings.Trim(strings.ToLower(id), "") - out := []Link{} +func (l *LinkRepository) Get(c *Container) []*Link { + id := utils.TruncateID(c.ID) + out := []*Link{} for _, link := range l.links { - if link.To == id || link.From == id { + if link.ToID == id || link.FromID == id { out = append(out, link) } } return out } -// Returns the link for a current alias -func (l *LinkRepository) GetByAlias(alias string) (Link, error) { - link, exists := l.links[alias] - if !exists { - return link, fmt.Errorf("Link does not exist for alias: %s", alias) - } - return link, nil +// Get a link based on the link's ID +func (l *LinkRepository) GetById(id string) *Link { + return l.links[id] } // Create a new link with a unique alias -func (l *LinkRepository) RegisterLink(link Link) error { - if _, exists := l.links[link.Alias]; exists { - return fmt.Errorf("A link for %s already exists", link.Alias) +func (l *LinkRepository) registerLink(link *Link) error { + if _, exists := l.links[link.ID()]; exists { + return fmt.Errorf("A link for %s already exists", link.ID()) } - utils.Debugf("Registering link: %v", link) - l.links[link.Alias] = link + utils.Debugf("Registering link: %s", link.ID()) + l.links[link.ID()] = link + + return nil +} + +// Disable and remote the link from the repository +func (l *LinkRepository) removeLink(link *Link) error { + link.Disable() + + utils.Debugf("Removing link: %s", link.ID()) + delete(l.links, link.ID()) return nil } diff --git a/utils.go b/utils.go index d526d372b..9ff0d00af 100644 --- a/utils.go +++ b/utils.go @@ -285,28 +285,6 @@ func migratePortMappings(config *Config) error { // Links come in the format of // id:port:alias -func parseLink(rawLink string) (Link, error) { - parts, err := utils.PartParser("id:port:alias", rawLink) - if err != nil { - return Link{}, err - } - port := NewPort(splitProtoPort(parts["port"])) - - return Link{ - From: parts["id"], - Alias: parts["alias"], - Port: port, - }, nil -} - -func parseLinks(rawLinks []string) ([]Link, error) { - out := make([]Link, len(rawLinks)) - for i, l := range rawLinks { - link, err := parseLink(l) - if err != nil { - return nil, err - } - out[i] = link - } - return out, nil +func parseLink(rawLink string) (map[string]string, error) { + return utils.PartParser("id:port:alias", rawLink) }