Browse code

Add flag `--host` to `service create` and `--host-add/--host-rm` to `service update`

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>

Yong Tang authored on 2016/11/04 00:05:00
Showing 10 changed files
... ...
@@ -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
+}