Browse Source

Merge pull request #7677 from erikh/update_hosts_linked_containers

Update /etc/hosts when linked container is restarted
Michael Crosby 11 years ago
parent
commit
2a5e29adc6

+ 32 - 4
daemon/container.go

@@ -297,6 +297,9 @@ func (container *Container) Start() (err error) {
 	if err := container.initializeNetworking(); err != nil {
 		return err
 	}
+	if err := container.updateParentsHosts(); err != nil {
+		return err
+	}
 	container.verifyDaemonSettings()
 	if err := prepareVolumesForContainer(container); err != nil {
 		return err
@@ -390,10 +393,7 @@ func (container *Container) buildHostnameFile() error {
 	return ioutil.WriteFile(container.HostnamePath, []byte(container.Config.Hostname+"\n"), 0644)
 }
 
-func (container *Container) buildHostnameAndHostsFiles(IP string) error {
-	if err := container.buildHostnameFile(); err != nil {
-		return err
-	}
+func (container *Container) buildHostsFiles(IP string) error {
 
 	hostsPath, err := container.getRootResourcePath("hosts")
 	if err != nil {
@@ -416,6 +416,14 @@ func (container *Container) buildHostnameAndHostsFiles(IP string) error {
 	return etchosts.Build(container.HostsPath, IP, container.Config.Hostname, container.Config.Domainname, &extraContent)
 }
 
+func (container *Container) buildHostnameAndHostsFiles(IP string) error {
+	if err := container.buildHostnameFile(); err != nil {
+		return err
+	}
+
+	return container.buildHostsFiles(IP)
+}
+
 func (container *Container) allocateNetwork() error {
 	mode := container.hostConfig.NetworkMode
 	if container.Config.NetworkDisabled || mode.IsContainer() || mode.IsHost() {
@@ -878,6 +886,26 @@ func (container *Container) setupContainerDns() error {
 	return ioutil.WriteFile(container.ResolvConfPath, resolvConf, 0644)
 }
 
+func (container *Container) updateParentsHosts() error {
+	parents, err := container.daemon.Parents(container.Name)
+	if err != nil {
+		return err
+	}
+	for _, cid := range parents {
+		if cid == "0" {
+			continue
+		}
+
+		c := container.daemon.Get(cid)
+		if c != nil && !container.daemon.config.DisableNetwork && !container.hostConfig.NetworkMode.IsContainer() && !container.hostConfig.NetworkMode.IsHost() {
+			if err := etchosts.Update(c.HostsPath, container.NetworkSettings.IPAddress, container.Name[1:]); err != nil {
+				return fmt.Errorf("Failed to update /etc/hosts in parent container: %v", err)
+			}
+		}
+	}
+	return nil
+}
+
 func (container *Container) initializeNetworking() error {
 	var err error
 	if container.hostConfig.NetworkMode.IsHost() {

+ 9 - 0
daemon/daemon.go

@@ -621,6 +621,15 @@ func (daemon *Daemon) Children(name string) (map[string]*Container, error) {
 	return children, nil
 }
 
+func (daemon *Daemon) Parents(name string) ([]string, error) {
+	name, err := GetFullContainerName(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return daemon.containerGraph.Parents(name)
+}
+
 func (daemon *Daemon) RegisterLink(parent, child *Container, alias string) error {
 	fullName := path.Join(parent.Name, alias)
 	if !daemon.containerGraph.Exists(fullName) {

+ 4 - 1
docs/sources/articles/networking.md

@@ -150,7 +150,10 @@ Four different options affect container domain name services.
     `CONTAINER_NAME`.  This lets processes inside the new container
     connect to the hostname `ALIAS` without having to know its IP.  The
     `--link=` option is discussed in more detail below, in the section
-    [Communication between containers](#between-containers).
+    [Communication between containers](#between-containers). Because
+    Docker may assign a different IP address to the linked containers
+    on restart, Docker updates the ALIAS entry in the /etc/hosts file
+    of the recipient containers.
 
  *  `--dns=IP_ADDRESS...` — sets the IP addresses added as `server`
     lines to the container's `/etc/resolv.conf` file.  Processes in the

+ 3 - 0
docs/sources/reference/run.md

@@ -432,6 +432,9 @@ mechanism to communicate with a linked container by its alias:
     $ docker run -d --name servicename busybox sleep 30
     $ docker run -i -t --link servicename:servicealias busybox ping -c 1 servicealias
 
+If you restart the source container (`servicename` in this case), the recipient
+container's `/etc/hosts` entry will be automatically updated.
+
 ## VOLUME (Shared Filesystems)
 
     -v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro].

+ 10 - 0
docs/sources/userguide/dockerlinks.md

@@ -241,6 +241,16 @@ to make use of your `db` container.
 > example, you could have multiple (differently named) web containers attached to your
 >`db` container.
 
+If you restart the source container, the linked containers `/etc/hosts` files
+will be automatically updated with the source container's new IP address,
+allowing linked communication to continue.
+
+    $ sudo docker restart db
+    root@aed84ee21bde:/opt/webapp# cat /etc/hosts
+    172.17.0.7  aed84ee21bde
+    . . .
+    172.17.0.9  db
+
 # Next step
 
 Now that you know how to link Docker containers together, the next step is

+ 47 - 0
integration-cli/docker_cli_run_test.go

@@ -1723,3 +1723,50 @@ func TestBindMounts(t *testing.T) {
 		t.Fatalf("Output should be %q, actual out: %q", expected, content)
 	}
 }
+
+func TestHostsLinkedContainerUpdate(t *testing.T) {
+	tmpdir, err := ioutil.TempDir("", "docker-integration")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpdir)
+
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", "c1", "busybox", "sleep", "5"))
+	if err != nil {
+		t.Fatal(err, out)
+	}
+
+	// TODO fix docker cp and /etc/hosts
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--link", "c1:c1", "--name", "c2", "busybox", "sh", "-c", "while true;do cp /etc/hosts /hosts; done"))
+	if err != nil {
+		t.Fatal(err, out)
+	}
+
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "cp", "c2:/hosts", tmpdir+"/1"))
+	if err != nil {
+		t.Fatal(err, out)
+	}
+
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "restart", "-t", "0", "c1"))
+	if err != nil {
+		t.Fatal(err, out)
+	}
+
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "cp", "c2:/hosts", tmpdir+"/2"))
+	if err != nil {
+		t.Fatal(err, out)
+	}
+
+	out, _, _, err = runCommandWithStdoutStderr(exec.Command("diff", tmpdir+"/1", tmpdir+"/2"))
+	if err == nil {
+		t.Fatalf("Expecting error, got none")
+	}
+	out = stripTrailingCharacters(out)
+	if out == "" {
+		t.Fatalf("expected /etc/hosts to be updated, but wasn't")
+	}
+
+	deleteAllContainers()
+
+	logDone("run - /etc/hosts updated in parent when restart")
+}

+ 34 - 0
pkg/graphdb/graphdb.go

@@ -281,6 +281,18 @@ func (db *Database) Children(name string, depth int) ([]WalkMeta, error) {
 	return db.children(e, name, depth, nil)
 }
 
+// Return the parents of a specified entity
+func (db *Database) Parents(name string) ([]string, error) {
+	db.mux.RLock()
+	defer db.mux.RUnlock()
+
+	e, err := db.get(name)
+	if err != nil {
+		return nil, err
+	}
+	return db.parents(e)
+}
+
 // Return the refrence count for a specified id
 func (db *Database) Refs(id string) int {
 	db.mux.RLock()
@@ -466,6 +478,28 @@ func (db *Database) children(e *Entity, name string, depth int, entities []WalkM
 	return entities, nil
 }
 
+func (db *Database) parents(e *Entity) (parents []string, err error) {
+	if e == nil {
+		return parents, nil
+	}
+
+	rows, err := db.conn.Query("SELECT parent_id FROM edge where entity_id = ?;", e.id)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var parentId string
+		if err := rows.Scan(&parentId); err != nil {
+			return nil, err
+		}
+		parents = append(parents, parentId)
+	}
+
+	return parents, nil
+}
+
 // Return the entity based on the parent path and name
 func (db *Database) child(parent *Entity, name string) *Entity {
 	var id string

+ 79 - 1
pkg/graphdb/graphdb_test.go

@@ -34,7 +34,7 @@ func TestNewDatabase(t *testing.T) {
 	defer destroyTestDb(dbpath)
 }
 
-func TestCreateRootEnity(t *testing.T) {
+func TestCreateRootEntity(t *testing.T) {
 	db, dbpath := newTestDb(t)
 	defer destroyTestDb(dbpath)
 	root := db.RootEntity()
@@ -94,6 +94,84 @@ func TestCreateChild(t *testing.T) {
 	}
 }
 
+func TestParents(t *testing.T) {
+	db, dbpath := newTestDb(t)
+	defer destroyTestDb(dbpath)
+
+	for i := 1; i < 6; i++ {
+		a := strconv.Itoa(i)
+		if _, err := db.Set("/"+a, a); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	for i := 6; i < 11; i++ {
+		a := strconv.Itoa(i)
+		p := strconv.Itoa(i - 5)
+
+		key := fmt.Sprintf("/%s/%s", p, a)
+
+		if _, err := db.Set(key, a); err != nil {
+			t.Fatal(err)
+		}
+
+		parents, err := db.Parents(key)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(parents) != 1 {
+			t.Fatalf("Expected 2 entries for %s got %d", key, len(parents))
+		}
+
+		if parents[0] != p {
+			t.Fatalf("ID %s received, %s expected", parents[0], p)
+		}
+	}
+}
+
+func TestChildren(t *testing.T) {
+	db, dbpath := newTestDb(t)
+	defer destroyTestDb(dbpath)
+
+	str := "/"
+	for i := 1; i < 6; i++ {
+		a := strconv.Itoa(i)
+		if _, err := db.Set(str+a, a); err != nil {
+			t.Fatal(err)
+		}
+
+		str = str + a + "/"
+	}
+
+	str = "/"
+	for i := 10; i < 30; i++ { // 20 entities
+		a := strconv.Itoa(i)
+		if _, err := db.Set(str+a, a); err != nil {
+			t.Fatal(err)
+		}
+
+		str = str + a + "/"
+	}
+	entries, err := db.Children("/", 5)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(entries) != 11 {
+		t.Fatalf("Expect 11 entries for / got %d", len(entries))
+	}
+
+	entries, err = db.Children("/", 20)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(entries) != 25 {
+		t.Fatalf("Expect 25 entries for / got %d", len(entries))
+	}
+}
+
 func TestListAllRootChildren(t *testing.T) {
 	db, dbpath := newTestDb(t)
 	defer destroyTestDb(dbpath)

+ 10 - 0
pkg/networkfs/etchosts/etchosts.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"io/ioutil"
+	"regexp"
 )
 
 var defaultContent = map[string]string{
@@ -41,3 +42,12 @@ func Build(path, IP, hostname, domainname string, extraContent *map[string]strin
 
 	return ioutil.WriteFile(path, content.Bytes(), 0644)
 }
+
+func Update(path, IP, hostname string) error {
+	old, err := ioutil.ReadFile(path)
+	if err != nil {
+		return err
+	}
+	var re = regexp.MustCompile(fmt.Sprintf("(\\S*)(\\t%s)", regexp.QuoteMeta(hostname)))
+	return ioutil.WriteFile(path, re.ReplaceAll(old, []byte(IP+"$2")), 0644)
+}

+ 34 - 0
pkg/networkfs/etchosts/etchosts_test.go

@@ -72,3 +72,37 @@ func TestBuildNoIP(t *testing.T) {
 		t.Fatalf("Expected to find '%s' got '%s'", expected, content)
 	}
 }
+
+func TestUpdate(t *testing.T) {
+	file, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(file.Name())
+
+	if err := Build(file.Name(), "10.11.12.13", "testhostname", "testdomainname", nil); err != nil {
+		t.Fatal(err)
+	}
+
+	content, err := ioutil.ReadFile(file.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if expected := "10.11.12.13\ttesthostname.testdomainname testhostname\n"; !bytes.Contains(content, []byte(expected)) {
+		t.Fatalf("Expected to find '%s' got '%s'", expected, content)
+	}
+
+	if err := Update(file.Name(), "1.1.1.1", "testhostname"); err != nil {
+		t.Fatal(err)
+	}
+
+	content, err = ioutil.ReadFile(file.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if expected := "1.1.1.1\ttesthostname.testdomainname testhostname\n"; !bytes.Contains(content, []byte(expected)) {
+		t.Fatalf("Expected to find '%s' got '%s'", expected, content)
+	}
+}