This fix tries to address 27902 by adding a flag `--host`
to `docker service create` and `--host-add/--host-rm` to
`docker service update`, so that it is possible to
specify extra `host:ip` settings in `/etc/hosts`.
This fix adds `Hosts` in swarmkit's `ContainerSpec` so that it
is possible to specify extra hosts during service creation.
Related docs has been updated.
An integration test has been added.
This fix fixes 27902.
Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
| ... | ... |
@@ -36,6 +36,10 @@ type ContainerSpec struct {
|
| 36 | 36 |
Mounts []mount.Mount `json:",omitempty"` |
| 37 | 37 |
StopGracePeriod *time.Duration `json:",omitempty"` |
| 38 | 38 |
Healthcheck *container.HealthConfig `json:",omitempty"` |
| 39 |
- DNSConfig *DNSConfig `json:",omitempty"` |
|
| 40 |
- Secrets []*SecretReference `json:",omitempty"` |
|
| 39 |
+ // The format of extra hosts on swarmkit is specified in: |
|
| 40 |
+ // http://man7.org/linux/man-pages/man5/hosts.5.html |
|
| 41 |
+ // IP_address canonical_hostname [aliases...] |
|
| 42 |
+ Hosts []string `json:",omitempty"` |
|
| 43 |
+ DNSConfig *DNSConfig `json:",omitempty"` |
|
| 44 |
+ Secrets []*SecretReference `json:",omitempty"` |
|
| 41 | 45 |
} |
| ... | ... |
@@ -45,6 +45,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 45 | 45 |
flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") |
| 46 | 46 |
flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") |
| 47 | 47 |
flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") |
| 48 |
+ flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") |
|
| 48 | 49 |
|
| 49 | 50 |
flags.SetInterspersed(false) |
| 50 | 51 |
return cmd |
| ... | ... |
@@ -397,6 +397,20 @@ func ValidatePort(value string) (string, error) {
|
| 397 | 397 |
return value, err |
| 398 | 398 |
} |
| 399 | 399 |
|
| 400 |
+// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli |
|
| 401 |
+// <host>:<ip> |
|
| 402 |
+// into a swarmkit host format: |
|
| 403 |
+// IP_address canonical_hostname [aliases...] |
|
| 404 |
+// This assumes input value (<host>:<ip>) has already been validated |
|
| 405 |
+func convertExtraHostsToSwarmHosts(extraHosts []string) []string {
|
|
| 406 |
+ hosts := []string{}
|
|
| 407 |
+ for _, extraHost := range extraHosts {
|
|
| 408 |
+ parts := strings.SplitN(extraHost, ":", 2) |
|
| 409 |
+ hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0]))
|
|
| 410 |
+ } |
|
| 411 |
+ return hosts |
|
| 412 |
+} |
|
| 413 |
+ |
|
| 400 | 414 |
type serviceOptions struct {
|
| 401 | 415 |
name string |
| 402 | 416 |
labels opts.ListOpts |
| ... | ... |
@@ -414,6 +428,7 @@ type serviceOptions struct {
|
| 414 | 414 |
dns opts.ListOpts |
| 415 | 415 |
dnsSearch opts.ListOpts |
| 416 | 416 |
dnsOption opts.ListOpts |
| 417 |
+ hosts opts.ListOpts |
|
| 417 | 418 |
|
| 418 | 419 |
resources resourceOptions |
| 419 | 420 |
stopGrace DurationOpt |
| ... | ... |
@@ -450,6 +465,7 @@ func newServiceOptions() *serviceOptions {
|
| 450 | 450 |
dns: opts.NewListOpts(opts.ValidateIPAddress), |
| 451 | 451 |
dnsOption: opts.NewListOpts(nil), |
| 452 | 452 |
dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), |
| 453 |
+ hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), |
|
| 453 | 454 |
networks: opts.NewListOpts(nil), |
| 454 | 455 |
} |
| 455 | 456 |
} |
| ... | ... |
@@ -498,6 +514,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
| 498 | 498 |
Search: opts.dnsSearch.GetAll(), |
| 499 | 499 |
Options: opts.dnsOption.GetAll(), |
| 500 | 500 |
}, |
| 501 |
+ Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), |
|
| 501 | 502 |
StopGracePeriod: opts.stopGrace.Value(), |
| 502 | 503 |
Secrets: nil, |
| 503 | 504 |
}, |
| ... | ... |
@@ -604,6 +621,9 @@ const ( |
| 604 | 604 |
flagDNSSearchRemove = "dns-search-rm" |
| 605 | 605 |
flagDNSSearchAdd = "dns-search-add" |
| 606 | 606 |
flagEndpointMode = "endpoint-mode" |
| 607 |
+ flagHost = "host" |
|
| 608 |
+ flagHostAdd = "host-add" |
|
| 609 |
+ flagHostRemove = "host-rm" |
|
| 607 | 610 |
flagHostname = "hostname" |
| 608 | 611 |
flagEnv = "env" |
| 609 | 612 |
flagEnvFile = "env-file" |
| ... | ... |
@@ -52,6 +52,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 52 | 52 |
flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") |
| 53 | 53 |
flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") |
| 54 | 54 |
flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") |
| 55 |
+ flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") |
|
| 55 | 56 |
flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") |
| 56 | 57 |
flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") |
| 57 | 58 |
flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") |
| ... | ... |
@@ -64,6 +65,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 64 | 64 |
flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") |
| 65 | 65 |
flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") |
| 66 | 66 |
flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") |
| 67 |
+ flags.Var(&opts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") |
|
| 67 | 68 |
|
| 68 | 69 |
return cmd |
| 69 | 70 |
} |
| ... | ... |
@@ -283,6 +285,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
|
| 283 | 283 |
} |
| 284 | 284 |
} |
| 285 | 285 |
|
| 286 |
+ if anyChanged(flags, flagHostAdd, flagHostRemove) {
|
|
| 287 |
+ if err := updateHosts(flags, &cspec.Hosts); err != nil {
|
|
| 288 |
+ return err |
|
| 289 |
+ } |
|
| 290 |
+ } |
|
| 291 |
+ |
|
| 286 | 292 |
if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
|
| 287 | 293 |
return err |
| 288 | 294 |
} |
| ... | ... |
@@ -683,6 +691,47 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error |
| 683 | 683 |
return nil |
| 684 | 684 |
} |
| 685 | 685 |
|
| 686 |
+func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
|
|
| 687 |
+ // Combine existing Hosts (in swarmkit format) with the host to add (convert to swarmkit format) |
|
| 688 |
+ if flags.Changed(flagHostAdd) {
|
|
| 689 |
+ values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetAll()) |
|
| 690 |
+ *hosts = append(*hosts, values...) |
|
| 691 |
+ } |
|
| 692 |
+ // Remove duplicate |
|
| 693 |
+ *hosts = removeDuplicates(*hosts) |
|
| 694 |
+ |
|
| 695 |
+ keysToRemove := make(map[string]struct{})
|
|
| 696 |
+ if flags.Changed(flagHostRemove) {
|
|
| 697 |
+ var empty struct{}
|
|
| 698 |
+ extraHostsToRemove := flags.Lookup(flagHostRemove).Value.(*opts.ListOpts).GetAll() |
|
| 699 |
+ for _, entry := range extraHostsToRemove {
|
|
| 700 |
+ key := strings.SplitN(entry, ":", 2)[0] |
|
| 701 |
+ keysToRemove[key] = empty |
|
| 702 |
+ } |
|
| 703 |
+ } |
|
| 704 |
+ |
|
| 705 |
+ newHosts := []string{}
|
|
| 706 |
+ for _, entry := range *hosts {
|
|
| 707 |
+ // Since this is in swarmkit format, we need to find the key, which is canonical_hostname of: |
|
| 708 |
+ // IP_address canonical_hostname [aliases...] |
|
| 709 |
+ parts := strings.Fields(entry) |
|
| 710 |
+ if len(parts) > 1 {
|
|
| 711 |
+ key := parts[1] |
|
| 712 |
+ if _, exists := keysToRemove[key]; !exists {
|
|
| 713 |
+ newHosts = append(newHosts, entry) |
|
| 714 |
+ } |
|
| 715 |
+ } else {
|
|
| 716 |
+ newHosts = append(newHosts, entry) |
|
| 717 |
+ } |
|
| 718 |
+ } |
|
| 719 |
+ |
|
| 720 |
+ // Sort so that result is predictable. |
|
| 721 |
+ sort.Strings(newHosts) |
|
| 722 |
+ |
|
| 723 |
+ *hosts = newHosts |
|
| 724 |
+ return nil |
|
| 725 |
+} |
|
| 726 |
+ |
|
| 686 | 727 |
// updateLogDriver updates the log driver only if the log driver flag is set. |
| 687 | 728 |
// All options will be replaced with those provided on the command line. |
| 688 | 729 |
func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
|
| ... | ... |
@@ -339,3 +339,23 @@ func TestUpdateHealthcheckTable(t *testing.T) {
|
| 339 | 339 |
} |
| 340 | 340 |
} |
| 341 | 341 |
} |
| 342 |
+ |
|
| 343 |
+func TestUpdateHosts(t *testing.T) {
|
|
| 344 |
+ flags := newUpdateCommand(nil).Flags() |
|
| 345 |
+ flags.Set("host-add", "example.net:2.2.2.2")
|
|
| 346 |
+ flags.Set("host-add", "ipv6.net:2001:db8:abc8::1")
|
|
| 347 |
+ // remove with ipv6 should work |
|
| 348 |
+ flags.Set("host-rm", "example.net:2001:db8:abc8::1")
|
|
| 349 |
+ // just hostname should work as well |
|
| 350 |
+ flags.Set("host-rm", "example.net")
|
|
| 351 |
+ // bad format error |
|
| 352 |
+ assert.Error(t, flags.Set("host-add", "$example.com$"), "bad format for add-host:")
|
|
| 353 |
+ |
|
| 354 |
+ hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"}
|
|
| 355 |
+ |
|
| 356 |
+ updateHosts(flags, &hosts) |
|
| 357 |
+ assert.Equal(t, len(hosts), 3) |
|
| 358 |
+ assert.Equal(t, hosts[0], "1.2.3.4 example.com") |
|
| 359 |
+ assert.Equal(t, hosts[1], "2001:db8:abc8::1 ipv6.net") |
|
| 360 |
+ assert.Equal(t, hosts[2], "4.3.2.1 example.org") |
|
| 361 |
+} |
| ... | ... |
@@ -24,6 +24,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
| 24 | 24 |
User: c.User, |
| 25 | 25 |
Groups: c.Groups, |
| 26 | 26 |
TTY: c.TTY, |
| 27 |
+ Hosts: c.Hosts, |
|
| 27 | 28 |
Secrets: secretReferencesFromGRPC(c.Secrets), |
| 28 | 29 |
} |
| 29 | 30 |
|
| ... | ... |
@@ -132,6 +133,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
| 132 | 132 |
User: c.User, |
| 133 | 133 |
Groups: c.Groups, |
| 134 | 134 |
TTY: c.TTY, |
| 135 |
+ Hosts: c.Hosts, |
|
| 135 | 136 |
Secrets: secretReferencesToGRPC(c.Secrets), |
| 136 | 137 |
} |
| 137 | 138 |
|
| ... | ... |
@@ -345,6 +345,20 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
|
| 345 | 345 |
hc.DNSOptions = c.spec().DNSConfig.Options |
| 346 | 346 |
} |
| 347 | 347 |
|
| 348 |
+ // The format of extra hosts on swarmkit is specified in: |
|
| 349 |
+ // http://man7.org/linux/man-pages/man5/hosts.5.html |
|
| 350 |
+ // IP_address canonical_hostname [aliases...] |
|
| 351 |
+ // However, the format of ExtraHosts in HostConfig is |
|
| 352 |
+ // <host>:<ip> |
|
| 353 |
+ // We need to do the conversion here |
|
| 354 |
+ // (Alias is ignored for now) |
|
| 355 |
+ for _, entry := range c.spec().Hosts {
|
|
| 356 |
+ parts := strings.Fields(entry) |
|
| 357 |
+ if len(parts) > 1 {
|
|
| 358 |
+ hc.ExtraHosts = append(hc.ExtraHosts, fmt.Sprintf("%s:%s", parts[1], parts[0]))
|
|
| 359 |
+ } |
|
| 360 |
+ } |
|
| 361 |
+ |
|
| 348 | 362 |
if c.task.LogDriver != nil {
|
| 349 | 363 |
hc.LogConfig = enginecontainer.LogConfig{
|
| 350 | 364 |
Type: c.task.LogDriver.Name, |
| ... | ... |
@@ -35,6 +35,7 @@ Options: |
| 35 | 35 |
--health-retries int Consecutive failures needed to report unhealthy |
| 36 | 36 |
--health-timeout duration Maximum time to allow one check to run (default none) |
| 37 | 37 |
--help Print usage |
| 38 |
+ --host list Set one or more custom host-to-IP mappings (host:ip) (default []) |
|
| 38 | 39 |
--hostname string Container hostname |
| 39 | 40 |
-l, --label list Service labels (default []) |
| 40 | 41 |
--limit-cpu decimal Limit CPUs (default 0.000) |
| ... | ... |
@@ -43,6 +43,8 @@ Options: |
| 43 | 43 |
--health-retries int Consecutive failures needed to report unhealthy |
| 44 | 44 |
--health-timeout duration Maximum time to allow one check to run (default none) |
| 45 | 45 |
--help Print usage |
| 46 |
+ --host-add list Add or update a custom host-to-IP mapping (host:ip) (default []) |
|
| 47 |
+ --host-rm list Remove a custom host-to-IP mapping (host:ip) (default []) |
|
| 46 | 48 |
--image string Service image tag |
| 47 | 49 |
--label-add list Add or update a service label (default []) |
| 48 | 50 |
--label-rm list Remove a label by its key (default []) |
| ... | ... |
@@ -1028,3 +1028,26 @@ func (s *DockerSwarmSuite) TestSwarmRotateUnlockKey(c *check.C) {
|
| 1028 | 1028 |
c.Assert(outs, checker.Not(checker.Contains), "Swarm is encrypted and needs to be unlocked") |
| 1029 | 1029 |
} |
| 1030 | 1030 |
} |
| 1031 |
+ |
|
| 1032 |
+func (s *DockerSwarmSuite) TestExtraHosts(c *check.C) {
|
|
| 1033 |
+ d := s.AddDaemon(c, true, true) |
|
| 1034 |
+ |
|
| 1035 |
+ // Create a service |
|
| 1036 |
+ name := "top" |
|
| 1037 |
+ _, err := d.Cmd("service", "create", "--name", name, "--host=example.com:1.2.3.4", "busybox", "top")
|
|
| 1038 |
+ c.Assert(err, checker.IsNil) |
|
| 1039 |
+ |
|
| 1040 |
+ // Make sure task has been deployed. |
|
| 1041 |
+ waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) |
|
| 1042 |
+ |
|
| 1043 |
+ // We need to get the container id. |
|
| 1044 |
+ out, err := d.Cmd("ps", "-a", "-q", "--no-trunc")
|
|
| 1045 |
+ c.Assert(err, checker.IsNil) |
|
| 1046 |
+ id := strings.TrimSpace(out) |
|
| 1047 |
+ |
|
| 1048 |
+ // Compare against expected output. |
|
| 1049 |
+ expectedOutput := "1.2.3.4\texample.com" |
|
| 1050 |
+ out, err = d.Cmd("exec", id, "cat", "/etc/hosts")
|
|
| 1051 |
+ c.Assert(err, checker.IsNil) |
|
| 1052 |
+ c.Assert(out, checker.Contains, expectedOutput, check.Commentf("Expected '%s', but got %q", expectedOutput, out))
|
|
| 1053 |
+} |