diff --git a/.changelog/11074.txt b/.changelog/11074.txt new file mode 100644 index 00000000000..07e047848e9 --- /dev/null +++ b/.changelog/11074.txt @@ -0,0 +1,3 @@ +```release-note:bug +docker: Append `extra_hosts` values from all tasks to `/etc/hosts`. +``` diff --git a/drivers/docker/driver.go b/drivers/docker/driver.go index 9682234607b..7711742c917 100644 --- a/drivers/docker/driver.go +++ b/drivers/docker/driver.go @@ -962,7 +962,7 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T // that comes from the pause container if task.NetworkIsolation != nil && driverConfig.NetworkMode == "" { etcHostMount, err := hostnames.GenerateEtcHostsMount( - task.AllocDir, task.NetworkIsolation, driverConfig.ExtraHosts) + task.Name, task.AllocDir, task.NetworkIsolation, driverConfig.ExtraHosts) if err != nil { return c, fmt.Errorf("failed to build mount for /etc/hosts: %v", err) } diff --git a/drivers/shared/hostnames/mount.go b/drivers/shared/hostnames/mount.go index 6a468097549..8315be41158 100644 --- a/drivers/shared/hostnames/mount.go +++ b/drivers/shared/hostnames/mount.go @@ -11,12 +11,27 @@ import ( "github.com/hashicorp/nomad/plugins/drivers" ) +const baseHostsFile = `# this file was generated by Nomad +127.0.0.1 localhost +::1 localhost +::1 ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +ff02::3 ip6-allhosts + +# this entry is the IP address and hostname of the allocation +# shared with tasks in the task group's network +%s %s +` + // GenerateEtcHostsMount writes a /etc/hosts file using the network spec's // hosts configuration, and returns a mount config so that task drivers can // bind-mount it into the resulting task's filesystem. The extraHosts // parameter is expected to be the same format as the extra_hosts field from // the Docker or containerd drivers: []string{":"} -func GenerateEtcHostsMount(taskDir string, conf *drivers.NetworkIsolationSpec, extraHosts []string) (*drivers.MountConfig, error) { +func GenerateEtcHostsMount(taskName string, taskDir string, conf *drivers.NetworkIsolationSpec, extraHosts []string) (*drivers.MountConfig, error) { if conf == nil || conf.Mode != drivers.NetIsolationModeGroup { return nil, nil } @@ -26,23 +41,26 @@ func GenerateEtcHostsMount(taskDir string, conf *drivers.NetworkIsolationSpec, e } var content strings.Builder - fmt.Fprintf(&content, `# this file was generated by Nomad -127.0.0.1 localhost -::1 localhost -::1 ip6-localhost ip6-loopback -fe00::0 ip6-localnet -ff00::0 ip6-mcastprefix -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters -ff02::3 ip6-allhosts -# this entry is the IP address and hostname of the allocation -# shared with tasks in the task group's network -%s %s -`, hostsCfg.Address, hostsCfg.Hostname) + path := filepath.Join(taskDir, "hosts") + _, err := os.Stat(path) + if err == nil { + // hosts file already exists so it was probably created by another task. + // Use it as the starting point. + hosts, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read hosts file %s: %v", path, err) + } + content.Write(hosts) + } else if os.IsNotExist(err) { + // hosts file doesn't exist, so create a new one from the base content. + fmt.Fprintf(&content, baseHostsFile, hostsCfg.Address, hostsCfg.Hostname) + } else { + return nil, fmt.Errorf("failed to read hosts file info: %v", err) + } if len(extraHosts) > 0 { - content.WriteString("\n# these entries are extra hosts added by the task config") + content.WriteString(fmt.Sprintf("\n# these entries are extra hosts added by the task %q", taskName)) for _, hostLine := range extraHosts { hostsEntry := strings.SplitN(hostLine, ":", 2) if len(hostsEntry) != 2 { @@ -56,15 +74,9 @@ ff02::3 ip6-allhosts content.WriteString("\n") } - path := filepath.Join(taskDir, "hosts") - - // tasks within an alloc should be able to share and modify the file, so - // only write to it if it doesn't exist - if _, err := os.Stat(path); os.IsNotExist(err) { - err := ioutil.WriteFile(path, []byte(content.String()), 0644) - if err != nil { - return nil, err - } + err = ioutil.WriteFile(path, []byte(content.String()), 0644) + if err != nil { + return nil, err } // Note that we're not setting readonly. The file is in the task dir diff --git a/drivers/shared/hostnames/mount_unix_test.go b/drivers/shared/hostnames/mount_unix_test.go index e7273b1d803..188610f2e9a 100644 --- a/drivers/shared/hostnames/mount_unix_test.go +++ b/drivers/shared/hostnames/mount_unix_test.go @@ -15,10 +15,14 @@ import ( func TestGenerateEtcHostsMount(t *testing.T) { + type task struct { + spec *drivers.NetworkIsolationSpec + extraHosts []string + } + testCases := []struct { name string - spec *drivers.NetworkIsolationSpec - extraHosts []string + tasks map[string]task expected []string expectedErr string }{ @@ -27,15 +31,23 @@ func TestGenerateEtcHostsMount(t *testing.T) { }, { name: "no-hosts-config", - spec: &drivers.NetworkIsolationSpec{Mode: drivers.NetIsolationModeGroup}, + tasks: map[string]task{ + "test": { + spec: &drivers.NetworkIsolationSpec{Mode: drivers.NetIsolationModeGroup}, + }, + }, }, { name: "base-case", - spec: &drivers.NetworkIsolationSpec{ - Mode: drivers.NetIsolationModeGroup, - HostsConfig: &drivers.HostsConfig{ - Address: "192.168.1.1", - Hostname: "xyzzy", + tasks: map[string]task{ + "test": { + spec: &drivers.NetworkIsolationSpec{ + Mode: drivers.NetIsolationModeGroup, + HostsConfig: &drivers.HostsConfig{ + Address: "192.168.1.1", + Hostname: "xyzzy", + }, + }, }, }, expected: []string{ @@ -44,45 +56,93 @@ func TestGenerateEtcHostsMount(t *testing.T) { }, { name: "with-valid-extra-hosts", - spec: &drivers.NetworkIsolationSpec{ - Mode: drivers.NetIsolationModeGroup, - HostsConfig: &drivers.HostsConfig{ - Address: "192.168.1.1", - Hostname: "xyzzy", + tasks: map[string]task{ + "test": { + spec: &drivers.NetworkIsolationSpec{ + Mode: drivers.NetIsolationModeGroup, + HostsConfig: &drivers.HostsConfig{ + Address: "192.168.1.1", + Hostname: "xyzzy", + }, + }, + extraHosts: []string{ + "apple:192.168.1.2", + "banana:2001:0db8:85a3:0000:0000:8a2e:0370:7334", + }, }, }, - extraHosts: []string{ - "apple:192.168.1.2", - "banana:2001:0db8:85a3:0000:0000:8a2e:0370:7334", + expected: []string{ + "192.168.1.1 xyzzy", + "192.168.1.2 apple", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334 banana", + }, + }, + { + name: "multiple-tasks-with-valid-extra-hosts", + tasks: map[string]task{ + "test-1": { + spec: &drivers.NetworkIsolationSpec{ + Mode: drivers.NetIsolationModeGroup, + HostsConfig: &drivers.HostsConfig{ + Address: "192.168.1.1", + Hostname: "xyzzy", + }, + }, + extraHosts: []string{ + "apple:192.168.1.2", + "banana:2001:0db8:85a3:0000:0000:8a2e:0370:7334", + }, + }, + "test-2": { + spec: &drivers.NetworkIsolationSpec{ + Mode: drivers.NetIsolationModeGroup, + HostsConfig: &drivers.HostsConfig{ + Address: "192.168.1.1", + Hostname: "xyzzy", + }, + }, + extraHosts: []string{ + "cherry:192.168.1.3", + }, + }, }, expected: []string{ "192.168.1.1 xyzzy", "192.168.1.2 apple", "2001:0db8:85a3:0000:0000:8a2e:0370:7334 banana", + "192.168.1.3 cherry", }, }, { name: "invalid-extra-hosts-syntax", - spec: &drivers.NetworkIsolationSpec{ - Mode: drivers.NetIsolationModeGroup, - HostsConfig: &drivers.HostsConfig{ - Address: "192.168.1.1", - Hostname: "xyzzy", + tasks: map[string]task{ + "test": { + spec: &drivers.NetworkIsolationSpec{ + Mode: drivers.NetIsolationModeGroup, + HostsConfig: &drivers.HostsConfig{ + Address: "192.168.1.1", + Hostname: "xyzzy", + }, + }, + extraHosts: []string{"apple192.168.1.2"}, }, }, - extraHosts: []string{"apple192.168.1.2"}, expectedErr: "invalid hosts entry \"apple192.168.1.2\"", }, { name: "invalid-extra-hosts-bad-ip", - spec: &drivers.NetworkIsolationSpec{ - Mode: drivers.NetIsolationModeGroup, - HostsConfig: &drivers.HostsConfig{ - Address: "192.168.1.1", - Hostname: "xyzzy", + tasks: map[string]task{ + "test": { + spec: &drivers.NetworkIsolationSpec{ + Mode: drivers.NetIsolationModeGroup, + HostsConfig: &drivers.HostsConfig{ + Address: "192.168.1.1", + Hostname: "xyzzy", + }, + }, + extraHosts: []string{"apple:192.168.1.256"}, }, }, - extraHosts: []string{"apple:192.168.1.256"}, expectedErr: "invalid IP address \"apple:192.168.1.256\"", }, } @@ -97,7 +157,10 @@ func TestGenerateEtcHostsMount(t *testing.T) { require.NoError(err) dest := filepath.Join(taskDir, "hosts") - got, err := GenerateEtcHostsMount(taskDir, tc.spec, tc.extraHosts) + var got *drivers.MountConfig + for taskName, taskConfig := range tc.tasks { + got, err = GenerateEtcHostsMount(taskName, taskDir, taskConfig.spec, taskConfig.extraHosts) + } if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) diff --git a/website/content/docs/upgrade/upgrade-specific.mdx b/website/content/docs/upgrade/upgrade-specific.mdx index c0dcd04c86e..7046f2ea706 100644 --- a/website/content/docs/upgrade/upgrade-specific.mdx +++ b/website/content/docs/upgrade/upgrade-specific.mdx @@ -44,6 +44,11 @@ from the task directory to `/etc/hosts` within the task. In Nomad 1.1.3 the source for the bind mount was moved to the allocation directory so that it is shared between all tasks in an allocation. +Please note that this change may prevent [`extra_hosts`] values to be correctly +propagated to each task when there are multiple tasks within the same group, +such as when using Consul Connect. Use a newer version of Nomad if this +affects you. + ## Nomad 1.1.0 #### Enterprise licenses @@ -1183,3 +1188,4 @@ deleted and then Nomad 0.3.0 can be launched. [cap_add_exec]: /docs/drivers/exec#cap_add [cap_drop_exec]: /docs/drivers/exec#cap_drop [`log_file`]: /docs/configuration#log_file +[`extra_hosts`]: /docs/drivers/docker#extra_hosts