Browse code

Update /etc/hosts when linked container is restarted

Docker-DCO-1.1-Signed-off-by: Victor Vieux <vieux@docker.com> (github: vieux)

Victor Vieux authored on 2014/07/15 08:19:37
Showing 8 changed files
... ...
@@ -297,6 +297,9 @@ func (container *Container) Start() (err error) {
297 297
 	if err := container.initializeNetworking(); err != nil {
298 298
 		return err
299 299
 	}
300
+	if err := container.updateParentsHosts(); err != nil {
301
+		return err
302
+	}
300 303
 	container.verifyDaemonSettings()
301 304
 	if err := prepareVolumesForContainer(container); err != nil {
302 305
 		return err
... ...
@@ -390,10 +393,7 @@ func (container *Container) buildHostnameFile() error {
390 390
 	return ioutil.WriteFile(container.HostnamePath, []byte(container.Config.Hostname+"\n"), 0644)
391 391
 }
392 392
 
393
-func (container *Container) buildHostnameAndHostsFiles(IP string) error {
394
-	if err := container.buildHostnameFile(); err != nil {
395
-		return err
396
-	}
393
+func (container *Container) buildHostsFiles(IP string) error {
397 394
 
398 395
 	hostsPath, err := container.getRootResourcePath("hosts")
399 396
 	if err != nil {
... ...
@@ -416,6 +416,14 @@ func (container *Container) buildHostnameAndHostsFiles(IP string) error {
416 416
 	return etchosts.Build(container.HostsPath, IP, container.Config.Hostname, container.Config.Domainname, &extraContent)
417 417
 }
418 418
 
419
+func (container *Container) buildHostnameAndHostsFiles(IP string) error {
420
+	if err := container.buildHostnameFile(); err != nil {
421
+		return err
422
+	}
423
+
424
+	return container.buildHostsFiles(IP)
425
+}
426
+
419 427
 func (container *Container) allocateNetwork() error {
420 428
 	mode := container.hostConfig.NetworkMode
421 429
 	if container.Config.NetworkDisabled || mode.IsContainer() || mode.IsHost() {
... ...
@@ -878,6 +886,26 @@ func (container *Container) setupContainerDns() error {
878 878
 	return ioutil.WriteFile(container.ResolvConfPath, resolvConf, 0644)
879 879
 }
880 880
 
881
+func (container *Container) updateParentsHosts() error {
882
+	parents, err := container.daemon.Parents(container.Name)
883
+	if err != nil {
884
+		return err
885
+	}
886
+	for _, cid := range parents {
887
+		if cid == "0" {
888
+			continue
889
+		}
890
+
891
+		c := container.daemon.Get(cid)
892
+		if c != nil && !container.daemon.config.DisableNetwork && !container.hostConfig.NetworkMode.IsContainer() && !container.hostConfig.NetworkMode.IsHost() {
893
+			if err := etchosts.Update(c.HostsPath, container.NetworkSettings.IPAddress, container.Name[1:]); err != nil {
894
+				return fmt.Errorf("Failed to update /etc/hosts in parent container: %v", err)
895
+			}
896
+		}
897
+	}
898
+	return nil
899
+}
900
+
881 901
 func (container *Container) initializeNetworking() error {
882 902
 	var err error
883 903
 	if container.hostConfig.NetworkMode.IsHost() {
... ...
@@ -621,6 +621,15 @@ func (daemon *Daemon) Children(name string) (map[string]*Container, error) {
621 621
 	return children, nil
622 622
 }
623 623
 
624
+func (daemon *Daemon) Parents(name string) ([]string, error) {
625
+	name, err := GetFullContainerName(name)
626
+	if err != nil {
627
+		return nil, err
628
+	}
629
+
630
+	return daemon.containerGraph.Parents(name)
631
+}
632
+
624 633
 func (daemon *Daemon) RegisterLink(parent, child *Container, alias string) error {
625 634
 	fullName := path.Join(parent.Name, alias)
626 635
 	if !daemon.containerGraph.Exists(fullName) {
... ...
@@ -150,7 +150,10 @@ Four different options affect container domain name services.
150 150
     `CONTAINER_NAME`.  This lets processes inside the new container
151 151
     connect to the hostname `ALIAS` without having to know its IP.  The
152 152
     `--link=` option is discussed in more detail below, in the section
153
-    [Communication between containers](#between-containers).
153
+    [Communication between containers](#between-containers).  Docker updates
154
+    the ALIAS entry in the /etc/hosts file of the recipient containers
155
+    in order to keep the link since Docker may assign a different IP
156
+    address to the linked containers on restart.
154 157
 
155 158
  *  `--dns=IP_ADDRESS...` — sets the IP addresses added as `server`
156 159
     lines to the container's `/etc/resolv.conf` file.  Processes in the
... ...
@@ -432,6 +432,9 @@ mechanism to communicate with a linked container by its alias:
432 432
     $ docker run -d --name servicename busybox sleep 30
433 433
     $ docker run -i -t --link servicename:servicealias busybox ping -c 1 servicealias
434 434
 
435
+If you restart the source container (`servicename` in this case), the recipient
436
+container's `/etc/hosts` entry will be automatically updated.
437
+
435 438
 ## VOLUME (Shared Filesystems)
436 439
 
437 440
     -v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro].
... ...
@@ -241,6 +241,16 @@ to make use of your `db` container.
241 241
 > example, you could have multiple (differently named) web containers attached to your
242 242
 >`db` container.
243 243
 
244
+If you restart the source container, the linked containers `/etc/hosts` files
245
+will be automatically updated with the source container's new IP address,
246
+allowing linked communication to continue.
247
+
248
+    $ sudo docker restart db
249
+    root@aed84ee21bde:/opt/webapp# cat /etc/hosts
250
+    172.17.0.7  aed84ee21bde
251
+    . . .
252
+    172.17.0.9  db
253
+
244 254
 # Next step
245 255
 
246 256
 Now that you know how to link Docker containers together, the next step is
... ...
@@ -1706,3 +1706,50 @@ func TestBindMounts(t *testing.T) {
1706 1706
 		t.Fatalf("Output should be %q, actual out: %q", expected, content)
1707 1707
 	}
1708 1708
 }
1709
+
1710
+func TestHostsLinkedContainerUpdate(t *testing.T) {
1711
+	tmpdir, err := ioutil.TempDir("", "docker-integration")
1712
+	if err != nil {
1713
+		t.Fatal(err)
1714
+	}
1715
+	defer os.RemoveAll(tmpdir)
1716
+
1717
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", "c1", "busybox", "sleep", "5"))
1718
+	if err != nil {
1719
+		t.Fatal(err, out)
1720
+	}
1721
+
1722
+	// TODO fix docker cp and /etc/hosts
1723
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--link", "c1:c1", "--name", "c2", "busybox", "sh", "-c", "while true;do cp /etc/hosts /hosts; done"))
1724
+	if err != nil {
1725
+		t.Fatal(err, out)
1726
+	}
1727
+
1728
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "cp", "c2:/hosts", tmpdir+"/1"))
1729
+	if err != nil {
1730
+		t.Fatal(err, out)
1731
+	}
1732
+
1733
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "restart", "-t", "0", "c1"))
1734
+	if err != nil {
1735
+		t.Fatal(err, out)
1736
+	}
1737
+
1738
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "cp", "c2:/hosts", tmpdir+"/2"))
1739
+	if err != nil {
1740
+		t.Fatal(err, out)
1741
+	}
1742
+
1743
+	out, _, _, err = runCommandWithStdoutStderr(exec.Command("diff", tmpdir+"/1", tmpdir+"/2"))
1744
+	if err == nil {
1745
+		t.Fatalf("Expecting error, got none")
1746
+	}
1747
+	out = stripTrailingCharacters(out)
1748
+	if out == "" {
1749
+		t.Fatalf("expected /etc/hosts to be updated, but wasn't")
1750
+	}
1751
+
1752
+	deleteAllContainers()
1753
+
1754
+	logDone("run - /etc/hosts updated in parent when restart")
1755
+}
... ...
@@ -281,6 +281,18 @@ func (db *Database) Children(name string, depth int) ([]WalkMeta, error) {
281 281
 	return db.children(e, name, depth, nil)
282 282
 }
283 283
 
284
+// Return the parents of a specified entity
285
+func (db *Database) Parents(name string) ([]string, error) {
286
+	db.mux.RLock()
287
+	defer db.mux.RUnlock()
288
+
289
+	e, err := db.get(name)
290
+	if err != nil {
291
+		return nil, err
292
+	}
293
+	return db.parents(e)
294
+}
295
+
284 296
 // Return the refrence count for a specified id
285 297
 func (db *Database) Refs(id string) int {
286 298
 	db.mux.RLock()
... ...
@@ -466,6 +478,28 @@ func (db *Database) children(e *Entity, name string, depth int, entities []WalkM
466 466
 	return entities, nil
467 467
 }
468 468
 
469
+func (db *Database) parents(e *Entity) (parents []string, err error) {
470
+	if e == nil {
471
+		return parents, nil
472
+	}
473
+
474
+	rows, err := db.conn.Query("SELECT parent_id FROM edge where entity_id = ?;", e.id)
475
+	if err != nil {
476
+		return nil, err
477
+	}
478
+	defer rows.Close()
479
+
480
+	for rows.Next() {
481
+		var parentId string
482
+		if err := rows.Scan(&parentId); err != nil {
483
+			return nil, err
484
+		}
485
+		parents = append(parents, parentId)
486
+	}
487
+
488
+	return parents, nil
489
+}
490
+
469 491
 // Return the entity based on the parent path and name
470 492
 func (db *Database) child(parent *Entity, name string) *Entity {
471 493
 	var id string
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"bytes"
5 5
 	"fmt"
6 6
 	"io/ioutil"
7
+	"regexp"
7 8
 )
8 9
 
9 10
 var defaultContent = map[string]string{
... ...
@@ -41,3 +42,12 @@ func Build(path, IP, hostname, domainname string, extraContent *map[string]strin
41 41
 
42 42
 	return ioutil.WriteFile(path, content.Bytes(), 0644)
43 43
 }
44
+
45
+func Update(path, IP, hostname string) error {
46
+	old, err := ioutil.ReadFile(path)
47
+	if err != nil {
48
+		return err
49
+	}
50
+	var re = regexp.MustCompile(fmt.Sprintf("(\\S*)(\\t%s)", regexp.QuoteMeta(hostname)))
51
+	return ioutil.WriteFile(path, re.ReplaceAll(old, []byte(IP+"$2")), 0644)
52
+}