Browse code

Add custom DNS settings to service definition

This fix tries to fix the issue raised in 24391 about allowing
custom DNS settings to service definition.

This fix adds `DNSConfig` (`Nameservers`, `Options`, `Search`) to
service definition, as well as `--dns`, `--dns-opt`, and `dns-search`
to `service create`.

An integration test has been added to cover the changes in this fix.

This fix fixes 24391.

A PR in swarmkit will be created separately.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/10/20 09:07:44
Showing 7 changed files
... ...
@@ -7,6 +7,20 @@ import (
7 7
 	"github.com/docker/docker/api/types/mount"
8 8
 )
9 9
 
10
+// DNSConfig specifies DNS related configurations in resolver configuration file (resolv.conf)
11
+// Detailed documentation is available in:
12
+// http://man7.org/linux/man-pages/man5/resolv.conf.5.html
13
+// `nameserver`, `search`, `options` have been supported.
14
+// TODO: `domain` is not supported yet.
15
+type DNSConfig struct {
16
+	// Nameservers specifies the IP addresses of the name servers
17
+	Nameservers []string `json:",omitempty"`
18
+	// Search specifies the search list for host-name lookup
19
+	Search []string `json:",omitempty"`
20
+	// Options allows certain internal resolver variables to be modified
21
+	Options []string `json:",omitempty"`
22
+}
23
+
10 24
 // ContainerSpec represents the spec of a container.
11 25
 type ContainerSpec struct {
12 26
 	Image           string                  `json:",omitempty"`
... ...
@@ -22,4 +36,5 @@ type ContainerSpec struct {
22 22
 	Mounts          []mount.Mount           `json:",omitempty"`
23 23
 	StopGracePeriod *time.Duration          `json:",omitempty"`
24 24
 	Healthcheck     *container.HealthConfig `json:",omitempty"`
25
+	DNSConfig       *DNSConfig              `json:",omitempty"`
25 26
 }
... ...
@@ -41,6 +41,9 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
41 41
 	flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
42 42
 	flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
43 43
 	flags.StringSliceVar(&opts.groups, flagGroup, []string{}, "Set one or more supplementary user groups for the container")
44
+	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
45
+	flags.Var(&opts.dnsOptions, flagDNSOptions, "Set DNS options")
46
+	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
44 47
 
45 48
 	flags.SetInterspersed(false)
46 49
 	return cmd
... ...
@@ -296,6 +296,9 @@ type serviceOptions struct {
296 296
 	groups          []string
297 297
 	tty             bool
298 298
 	mounts          opts.MountOpt
299
+	dns             opts.ListOpts
300
+	dnsSearch       opts.ListOpts
301
+	dnsOptions      opts.ListOpts
299 302
 
300 303
 	resources resourceOptions
301 304
 	stopGrace DurationOpt
... ...
@@ -325,7 +328,10 @@ func newServiceOptions() *serviceOptions {
325 325
 		endpoint: endpointOptions{
326 326
 			ports: opts.NewListOpts(ValidatePort),
327 327
 		},
328
-		logDriver: newLogDriverOptions(),
328
+		logDriver:  newLogDriverOptions(),
329
+		dns:        opts.NewListOpts(opts.ValidateIPAddress),
330
+		dnsOptions: opts.NewListOpts(nil),
331
+		dnsSearch:  opts.NewListOpts(opts.ValidateDNSSearch),
329 332
 	}
330 333
 }
331 334
 
... ...
@@ -358,16 +364,21 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
358 358
 		},
359 359
 		TaskTemplate: swarm.TaskSpec{
360 360
 			ContainerSpec: swarm.ContainerSpec{
361
-				Image:           opts.image,
362
-				Args:            opts.args,
363
-				Env:             currentEnv,
364
-				Hostname:        opts.hostname,
365
-				Labels:          runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
366
-				Dir:             opts.workdir,
367
-				User:            opts.user,
368
-				Groups:          opts.groups,
369
-				TTY:             opts.tty,
370
-				Mounts:          opts.mounts.Value(),
361
+				Image:    opts.image,
362
+				Args:     opts.args,
363
+				Env:      currentEnv,
364
+				Hostname: opts.hostname,
365
+				Labels:   runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
366
+				Dir:      opts.workdir,
367
+				User:     opts.user,
368
+				Groups:   opts.groups,
369
+				TTY:      opts.tty,
370
+				Mounts:   opts.mounts.Value(),
371
+				DNSConfig: &swarm.DNSConfig{
372
+					Nameservers: opts.dns.GetAll(),
373
+					Search:      opts.dnsSearch.GetAll(),
374
+					Options:     opts.dnsOptions.GetAll(),
375
+				},
371 376
 				StopGracePeriod: opts.stopGrace.Value(),
372 377
 			},
373 378
 			Networks:      convertNetworks(opts.networks),
... ...
@@ -463,6 +474,9 @@ const (
463 463
 	flagContainerLabel        = "container-label"
464 464
 	flagContainerLabelRemove  = "container-label-rm"
465 465
 	flagContainerLabelAdd     = "container-label-add"
466
+	flagDNS                   = "dns"
467
+	flagDNSOptions            = "dns-opt"
468
+	flagDNSSearch             = "dns-search"
466 469
 	flagEndpointMode          = "endpoint-mode"
467 470
 	flagHostname              = "hostname"
468 471
 	flagEnv                   = "env"
... ...
@@ -25,6 +25,14 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
25 25
 		TTY:      c.TTY,
26 26
 	}
27 27
 
28
+	if c.DNSConfig != nil {
29
+		containerSpec.DNSConfig = &types.DNSConfig{
30
+			Nameservers: c.DNSConfig.Nameservers,
31
+			Search:      c.DNSConfig.Search,
32
+			Options:     c.DNSConfig.Options,
33
+		}
34
+	}
35
+
28 36
 	// Mounts
29 37
 	for _, m := range c.Mounts {
30 38
 		mount := mounttypes.Mount{
... ...
@@ -81,6 +89,14 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
81 81
 		TTY:      c.TTY,
82 82
 	}
83 83
 
84
+	if c.DNSConfig != nil {
85
+		containerSpec.DNSConfig = &swarmapi.ContainerSpec_DNSConfig{
86
+			Nameservers: c.DNSConfig.Nameservers,
87
+			Search:      c.DNSConfig.Search,
88
+			Options:     c.DNSConfig.Options,
89
+		}
90
+	}
91
+
84 92
 	if c.StopGracePeriod != nil {
85 93
 		containerSpec.StopGracePeriod = ptypes.DurationProto(*c.StopGracePeriod)
86 94
 	}
... ...
@@ -327,6 +327,12 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
327 327
 		GroupAdd:  c.spec().Groups,
328 328
 	}
329 329
 
330
+	if c.spec().DNSConfig != nil {
331
+		hc.DNS = c.spec().DNSConfig.Nameservers
332
+		hc.DNSSearch = c.spec().DNSConfig.Search
333
+		hc.DNSOptions = c.spec().DNSConfig.Options
334
+	}
335
+
330 336
 	if c.task.LogDriver != nil {
331 337
 		hc.LogConfig = enginecontainer.LogConfig{
332 338
 			Type:   c.task.LogDriver.Name,
... ...
@@ -23,6 +23,9 @@ Create a new service
23 23
 Options:
24 24
       --constraint value                 Placement constraints (default [])
25 25
       --container-label value            Service container labels (default [])
26
+      --dns list                         Set custom DNS servers (default [])
27
+      --dns-opt list                     Set DNS options (default [])
28
+      --dns-search list                  Set custom DNS search domains (default [])
26 29
       --endpoint-mode string             Endpoint mode (vip or dnsrr)
27 30
   -e, --env value                        Set environment variables (default [])
28 31
       --env-file value                   Read in a file of environment variables (default [])
... ...
@@ -789,3 +789,30 @@ func (s *DockerSwarmSuite) TestSwarmServiceTTYUpdate(c *check.C) {
789 789
 	c.Assert(err, checker.IsNil)
790 790
 	c.Assert(strings.TrimSpace(out), checker.Equals, "true")
791 791
 }
792
+
793
+func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) {
794
+	d := s.AddDaemon(c, true, true)
795
+
796
+	// Create a service
797
+	name := "top"
798
+	_, err := d.Cmd("service", "create", "--name", name, "--dns=1.2.3.4", "--dns-search=example.com", "--dns-opt=timeout:3", "busybox", "top")
799
+	c.Assert(err, checker.IsNil)
800
+
801
+	// Make sure task has been deployed.
802
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
803
+
804
+	// We need to get the container id.
805
+	out, err := d.Cmd("ps", "-a", "-q", "--no-trunc")
806
+	c.Assert(err, checker.IsNil)
807
+	id := strings.TrimSpace(out)
808
+
809
+	// Compare against expected output.
810
+	expectedOutput1 := "nameserver 1.2.3.4"
811
+	expectedOutput2 := "search example.com"
812
+	expectedOutput3 := "options timeout:3"
813
+	out, err = d.Cmd("exec", id, "cat", "/etc/resolv.conf")
814
+	c.Assert(err, checker.IsNil)
815
+	c.Assert(out, checker.Contains, expectedOutput1, check.Commentf("Expected '%s', but got %q", expectedOutput1, out))
816
+	c.Assert(out, checker.Contains, expectedOutput2, check.Commentf("Expected '%s', but got %q", expectedOutput2, out))
817
+	c.Assert(out, checker.Contains, expectedOutput3, check.Commentf("Expected '%s', but got %q", expectedOutput3, out))
818
+}