diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index 39f1d3987c..0db5cbcb81 100644 --- a/api/types/swarm/container.go +++ b/api/types/swarm/container.go @@ -36,6 +36,10 @@ type ContainerSpec struct { Mounts []mount.Mount `json:",omitempty"` StopGracePeriod *time.Duration `json:",omitempty"` Healthcheck *container.HealthConfig `json:",omitempty"` - DNSConfig *DNSConfig `json:",omitempty"` - Secrets []*SecretReference `json:",omitempty"` + // The format of extra hosts on swarmkit is specified in: + // http://man7.org/linux/man-pages/man5/hosts.5.html + // IP_address canonical_hostname [aliases...] + Hosts []string `json:",omitempty"` + DNSConfig *DNSConfig `json:",omitempty"` + Secrets []*SecretReference `json:",omitempty"` } diff --git a/cli/command/service/create.go b/cli/command/service/create.go index 3efee10f41..17cf19625f 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -45,6 +45,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") + flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") flags.SetInterspersed(false) return cmd diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index c48c952e0c..52971ae833 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -397,6 +397,20 @@ func ValidatePort(value string) (string, error) { return value, err } +// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli +// : +// into a swarmkit host format: +// IP_address canonical_hostname [aliases...] +// This assumes input value (:) has already been validated +func convertExtraHostsToSwarmHosts(extraHosts []string) []string { + hosts := []string{} + for _, extraHost := range extraHosts { + parts := strings.SplitN(extraHost, ":", 2) + hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0])) + } + return hosts +} + type serviceOptions struct { name string labels opts.ListOpts @@ -414,6 +428,7 @@ type serviceOptions struct { dns opts.ListOpts dnsSearch opts.ListOpts dnsOption opts.ListOpts + hosts opts.ListOpts resources resourceOptions stopGrace DurationOpt @@ -450,6 +465,7 @@ func newServiceOptions() *serviceOptions { dns: opts.NewListOpts(opts.ValidateIPAddress), dnsOption: opts.NewListOpts(nil), dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), networks: opts.NewListOpts(nil), } } @@ -498,6 +514,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Search: opts.dnsSearch.GetAll(), Options: opts.dnsOption.GetAll(), }, + Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), StopGracePeriod: opts.stopGrace.Value(), Secrets: nil, }, @@ -604,6 +621,9 @@ const ( flagDNSSearchRemove = "dns-search-rm" flagDNSSearchAdd = "dns-search-add" flagEndpointMode = "endpoint-mode" + flagHost = "host" + flagHostAdd = "host-add" + flagHostRemove = "host-rm" flagHostname = "hostname" flagEnv = "env" flagEnvFile = "env-file" diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 9741f67d54..f5acc2c511 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -52,6 +52,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") + flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") @@ -64,6 +65,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") + flags.Var(&opts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") return cmd } @@ -283,6 +285,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } + if anyChanged(flags, flagHostAdd, flagHostRemove) { + if err := updateHosts(flags, &cspec.Hosts); err != nil { + return err + } + } + if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil { return err } @@ -683,6 +691,47 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error return nil } +func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { + // Combine existing Hosts (in swarmkit format) with the host to add (convert to swarmkit format) + if flags.Changed(flagHostAdd) { + values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetAll()) + *hosts = append(*hosts, values...) + } + // Remove duplicate + *hosts = removeDuplicates(*hosts) + + keysToRemove := make(map[string]struct{}) + if flags.Changed(flagHostRemove) { + var empty struct{} + extraHostsToRemove := flags.Lookup(flagHostRemove).Value.(*opts.ListOpts).GetAll() + for _, entry := range extraHostsToRemove { + key := strings.SplitN(entry, ":", 2)[0] + keysToRemove[key] = empty + } + } + + newHosts := []string{} + for _, entry := range *hosts { + // Since this is in swarmkit format, we need to find the key, which is canonical_hostname of: + // IP_address canonical_hostname [aliases...] + parts := strings.Fields(entry) + if len(parts) > 1 { + key := parts[1] + if _, exists := keysToRemove[key]; !exists { + newHosts = append(newHosts, entry) + } + } else { + newHosts = append(newHosts, entry) + } + } + + // Sort so that result is predictable. + sort.Strings(newHosts) + + *hosts = newHosts + return nil +} + // updateLogDriver updates the log driver only if the log driver flag is set. // All options will be replaced with those provided on the command line. func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index b99064352a..a3736090ae 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -339,3 +339,23 @@ func TestUpdateHealthcheckTable(t *testing.T) { } } } + +func TestUpdateHosts(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("host-add", "example.net:2.2.2.2") + flags.Set("host-add", "ipv6.net:2001:db8:abc8::1") + // remove with ipv6 should work + flags.Set("host-rm", "example.net:2001:db8:abc8::1") + // just hostname should work as well + flags.Set("host-rm", "example.net") + // bad format error + assert.Error(t, flags.Set("host-add", "$example.com$"), "bad format for add-host:") + + hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"} + + updateHosts(flags, &hosts) + assert.Equal(t, len(hosts), 3) + assert.Equal(t, hosts[0], "1.2.3.4 example.com") + assert.Equal(t, hosts[1], "2001:db8:abc8::1 ipv6.net") + assert.Equal(t, hosts[2], "4.3.2.1 example.org") +} diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index a1ecabe92a..ebd80d09e6 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -24,6 +24,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { User: c.User, Groups: c.Groups, TTY: c.TTY, + Hosts: c.Hosts, Secrets: secretReferencesFromGRPC(c.Secrets), } @@ -132,6 +133,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { User: c.User, Groups: c.Groups, TTY: c.TTY, + Hosts: c.Hosts, Secrets: secretReferencesToGRPC(c.Secrets), } diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index ab24d072a8..7a93ecad01 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -345,6 +345,20 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig { hc.DNSOptions = c.spec().DNSConfig.Options } + // The format of extra hosts on swarmkit is specified in: + // http://man7.org/linux/man-pages/man5/hosts.5.html + // IP_address canonical_hostname [aliases...] + // However, the format of ExtraHosts in HostConfig is + // : + // We need to do the conversion here + // (Alias is ignored for now) + for _, entry := range c.spec().Hosts { + parts := strings.Fields(entry) + if len(parts) > 1 { + hc.ExtraHosts = append(hc.ExtraHosts, fmt.Sprintf("%s:%s", parts[1], parts[0])) + } + } + if c.task.LogDriver != nil { hc.LogConfig = enginecontainer.LogConfig{ Type: c.task.LogDriver.Name, diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 2893c84252..9f441c05fe 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -35,6 +35,7 @@ Options: --health-retries int Consecutive failures needed to report unhealthy --health-timeout duration Maximum time to allow one check to run (default none) --help Print usage + --host list Set one or more custom host-to-IP mappings (host:ip) (default []) --hostname string Container hostname -l, --label list Service labels (default []) --limit-cpu decimal Limit CPUs (default 0.000) diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index c5104103cc..51a9dd3dda 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -43,6 +43,8 @@ Options: --health-retries int Consecutive failures needed to report unhealthy --health-timeout duration Maximum time to allow one check to run (default none) --help Print usage + --host-add list Add or update a custom host-to-IP mapping (host:ip) (default []) + --host-rm list Remove a custom host-to-IP mapping (host:ip) (default []) --image string Service image tag --label-add list Add or update a service label (default []) --label-rm list Remove a label by its key (default []) diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index 8814cd2526..148b39fa51 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -1028,3 +1028,26 @@ func (s *DockerSwarmSuite) TestSwarmRotateUnlockKey(c *check.C) { c.Assert(outs, checker.Not(checker.Contains), "Swarm is encrypted and needs to be unlocked") } } + +func (s *DockerSwarmSuite) TestExtraHosts(c *check.C) { + d := s.AddDaemon(c, true, true) + + // Create a service + name := "top" + _, err := d.Cmd("service", "create", "--name", name, "--host=example.com:1.2.3.4", "busybox", "top") + c.Assert(err, checker.IsNil) + + // Make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) + + // We need to get the container id. + out, err := d.Cmd("ps", "-a", "-q", "--no-trunc") + c.Assert(err, checker.IsNil) + id := strings.TrimSpace(out) + + // Compare against expected output. + expectedOutput := "1.2.3.4\texample.com" + out, err = d.Cmd("exec", id, "cat", "/etc/hosts") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, expectedOutput, check.Commentf("Expected '%s', but got %q", expectedOutput, out)) +}