From ac32065d5ca5db1d387131774f66505383f21843 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 14 Jul 2014 23:19:37 +0000 Subject: [PATCH 1/4] Update /etc/hosts when linked container is restarted Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) Upstream-commit: 450740c891c60dda15b7abe29f35b8e142c2c40e Component: engine --- components/engine/daemon/container.go | 36 ++++++++++++-- components/engine/daemon/daemon.go | 9 ++++ .../docs/sources/articles/networking.md | 5 +- .../engine/docs/sources/reference/run.md | 3 ++ .../docs/sources/userguide/dockerlinks.md | 10 ++++ .../integration-cli/docker_cli_run_test.go | 47 +++++++++++++++++++ components/engine/pkg/graphdb/graphdb.go | 34 ++++++++++++++ .../engine/pkg/networkfs/etchosts/etchosts.go | 10 ++++ 8 files changed, 149 insertions(+), 5 deletions(-) diff --git a/components/engine/daemon/container.go b/components/engine/daemon/container.go index 59571ed814..23f5f999eb 100644 --- a/components/engine/daemon/container.go +++ b/components/engine/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() { diff --git a/components/engine/daemon/daemon.go b/components/engine/daemon/daemon.go index 968844e3d2..8ff79801c8 100644 --- a/components/engine/daemon/daemon.go +++ b/components/engine/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) { diff --git a/components/engine/docs/sources/articles/networking.md b/components/engine/docs/sources/articles/networking.md index f9aa2d26d3..43443e0e18 100644 --- a/components/engine/docs/sources/articles/networking.md +++ b/components/engine/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). Docker updates + the ALIAS entry in the /etc/hosts file of the recipient containers + in order to keep the link since Docker may assign a different IP + address to the linked containers on restart. * `--dns=IP_ADDRESS...` — sets the IP addresses added as `server` lines to the container's `/etc/resolv.conf` file. Processes in the diff --git a/components/engine/docs/sources/reference/run.md b/components/engine/docs/sources/reference/run.md index a933a32bea..6dc7b40721 100644 --- a/components/engine/docs/sources/reference/run.md +++ b/components/engine/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]. diff --git a/components/engine/docs/sources/userguide/dockerlinks.md b/components/engine/docs/sources/userguide/dockerlinks.md index 3624bf72c3..7a5c1514d0 100644 --- a/components/engine/docs/sources/userguide/dockerlinks.md +++ b/components/engine/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 diff --git a/components/engine/integration-cli/docker_cli_run_test.go b/components/engine/integration-cli/docker_cli_run_test.go index 7de2d334b1..a080788dd2 100644 --- a/components/engine/integration-cli/docker_cli_run_test.go +++ b/components/engine/integration-cli/docker_cli_run_test.go @@ -1706,3 +1706,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") +} diff --git a/components/engine/pkg/graphdb/graphdb.go b/components/engine/pkg/graphdb/graphdb.go index d0e049b07f..59873fefb3 100644 --- a/components/engine/pkg/graphdb/graphdb.go +++ b/components/engine/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 diff --git a/components/engine/pkg/networkfs/etchosts/etchosts.go b/components/engine/pkg/networkfs/etchosts/etchosts.go index 144a039bff..6cf29b046f 100644 --- a/components/engine/pkg/networkfs/etchosts/etchosts.go +++ b/components/engine/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) +} From 303526eefde1da33d3d490eb51378f1c2aecd66c Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Mon, 25 Aug 2014 22:19:00 -0700 Subject: [PATCH 2/4] pkg/graphdb: tests for Parents and Children Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) Upstream-commit: 925a3254e757407c5bfea5575df3b807f6596e70 Component: engine --- components/engine/pkg/graphdb/graphdb_test.go | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/components/engine/pkg/graphdb/graphdb_test.go b/components/engine/pkg/graphdb/graphdb_test.go index 8cddd0bf48..7568e66de4 100644 --- a/components/engine/pkg/graphdb/graphdb_test.go +++ b/components/engine/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) From 31ac70b2bece4cf3c4e07e66f26361bfddf2bfc2 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Mon, 25 Aug 2014 22:23:56 -0700 Subject: [PATCH 3/4] pkg/networkfs/etchosts: tests for Update method Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) Upstream-commit: 9f09ef18a44d6e14da3d8a60d923de9e38ffc33e Component: engine --- .../pkg/networkfs/etchosts/etchosts_test.go | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/components/engine/pkg/networkfs/etchosts/etchosts_test.go b/components/engine/pkg/networkfs/etchosts/etchosts_test.go index 44406c81b8..05a4f447f7 100644 --- a/components/engine/pkg/networkfs/etchosts/etchosts_test.go +++ b/components/engine/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) + } +} From 9d40e69de70ecd40a212593f83416e81ea424bc6 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Tue, 26 Aug 2014 10:59:22 -0700 Subject: [PATCH 4/4] Update networking doc with clarified text regarding links usage Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) Upstream-commit: f6a2fc4c3370983043a4284be5d8a09c3ff3bb6e Component: engine --- components/engine/docs/sources/articles/networking.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/engine/docs/sources/articles/networking.md b/components/engine/docs/sources/articles/networking.md index 43443e0e18..d61ebd65ba 100644 --- a/components/engine/docs/sources/articles/networking.md +++ b/components/engine/docs/sources/articles/networking.md @@ -150,10 +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). Docker updates - the ALIAS entry in the /etc/hosts file of the recipient containers - in order to keep the link since Docker may assign a different IP - address to the linked containers on restart. + [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