Browse code

Merge pull request #27567 from yongtang/24391-dns-setting

Add custom DNS settings to service definition

Victor Vieux authored on 2016/11/09 10:43:00
Showing 12 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,15 @@ const (
463 463
 	flagContainerLabel        = "container-label"
464 464
 	flagContainerLabelRemove  = "container-label-rm"
465 465
 	flagContainerLabelAdd     = "container-label-add"
466
+	flagDNS                   = "dns"
467
+	flagDNSRemove             = "dns-rm"
468
+	flagDNSAdd                = "dns-add"
469
+	flagDNSOptions            = "dns-options"
470
+	flagDNSOptionsRemove      = "dns-options-rm"
471
+	flagDNSOptionsAdd         = "dns-options-add"
472
+	flagDNSSearch             = "dns-search"
473
+	flagDNSSearchRemove       = "dns-search-rm"
474
+	flagDNSSearchAdd          = "dns-search-add"
466 475
 	flagEndpointMode          = "endpoint-mode"
467 476
 	flagHostname              = "hostname"
468 477
 	flagEnv                   = "env"
... ...
@@ -48,6 +48,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
48 48
 	flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
49 49
 	flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port")
50 50
 	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
51
+	flags.Var(newListOptsVar(), flagDNSRemove, "Remove custom DNS servers")
52
+	flags.Var(newListOptsVar(), flagDNSOptionsRemove, "Remove DNS options")
53
+	flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove DNS search domains")
51 54
 	flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label")
52 55
 	flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
53 56
 	flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
... ...
@@ -55,6 +58,10 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
55 55
 	flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update a placement constraint")
56 56
 	flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
57 57
 	flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add an additional supplementary user group to the container")
58
+	flags.Var(&opts.dns, flagDNSAdd, "Add or update custom DNS servers")
59
+	flags.Var(&opts.dnsOptions, flagDNSOptionsAdd, "Add or update DNS options")
60
+	flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update custom DNS search domains")
61
+
58 62
 	return cmd
59 63
 }
60 64
 
... ...
@@ -257,6 +264,15 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
257 257
 		}
258 258
 	}
259 259
 
260
+	if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionsAdd, flagDNSOptionsRemove, flagDNSSearchAdd, flagDNSSearchRemove) {
261
+		if cspec.DNSConfig == nil {
262
+			cspec.DNSConfig = &swarm.DNSConfig{}
263
+		}
264
+		if err := updateDNSConfig(flags, &cspec.DNSConfig); err != nil {
265
+			return err
266
+		}
267
+	}
268
+
260 269
 	if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
261 270
 		return err
262 271
 	}
... ...
@@ -484,6 +500,71 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error {
484 484
 	return nil
485 485
 }
486 486
 
487
+func removeDuplicates(entries []string) []string {
488
+	hit := map[string]bool{}
489
+	newEntries := []string{}
490
+	for _, v := range entries {
491
+		if !hit[v] {
492
+			newEntries = append(newEntries, v)
493
+			hit[v] = true
494
+		}
495
+	}
496
+	return newEntries
497
+}
498
+
499
+func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
500
+	newConfig := &swarm.DNSConfig{}
501
+
502
+	nameservers := (*config).Nameservers
503
+	if flags.Changed(flagDNSAdd) {
504
+		values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetAll()
505
+		nameservers = append(nameservers, values...)
506
+	}
507
+	nameservers = removeDuplicates(nameservers)
508
+	toRemove := buildToRemoveSet(flags, flagDNSRemove)
509
+	for _, nameserver := range nameservers {
510
+		if _, exists := toRemove[nameserver]; !exists {
511
+			newConfig.Nameservers = append(newConfig.Nameservers, nameserver)
512
+
513
+		}
514
+	}
515
+	// Sort so that result is predictable.
516
+	sort.Strings(newConfig.Nameservers)
517
+
518
+	search := (*config).Search
519
+	if flags.Changed(flagDNSSearchAdd) {
520
+		values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetAll()
521
+		search = append(search, values...)
522
+	}
523
+	search = removeDuplicates(search)
524
+	toRemove = buildToRemoveSet(flags, flagDNSSearchRemove)
525
+	for _, entry := range search {
526
+		if _, exists := toRemove[entry]; !exists {
527
+			newConfig.Search = append(newConfig.Search, entry)
528
+		}
529
+	}
530
+	// Sort so that result is predictable.
531
+	sort.Strings(newConfig.Search)
532
+
533
+	options := (*config).Options
534
+	if flags.Changed(flagDNSOptionsAdd) {
535
+		values := flags.Lookup(flagDNSOptionsAdd).Value.(*opts.ListOpts).GetAll()
536
+		options = append(options, values...)
537
+	}
538
+	options = removeDuplicates(options)
539
+	toRemove = buildToRemoveSet(flags, flagDNSOptionsRemove)
540
+	for _, option := range options {
541
+		if _, exists := toRemove[option]; !exists {
542
+			newConfig.Options = append(newConfig.Options, option)
543
+		}
544
+	}
545
+	// Sort so that result is predictable.
546
+	sort.Strings(newConfig.Options)
547
+
548
+	*config = newConfig
549
+	return nil
550
+}
551
+
487 552
 type byPortConfig []swarm.PortConfig
488 553
 
489 554
 func (r byPortConfig) Len() int      { return len(r) }
... ...
@@ -120,6 +120,52 @@ func TestUpdateGroups(t *testing.T) {
120 120
 	assert.Equal(t, groups[2], "wheel")
121 121
 }
122 122
 
123
+func TestUpdateDNSConfig(t *testing.T) {
124
+	flags := newUpdateCommand(nil).Flags()
125
+
126
+	// IPv4, with duplicates
127
+	flags.Set("dns-add", "1.1.1.1")
128
+	flags.Set("dns-add", "1.1.1.1")
129
+	flags.Set("dns-add", "2.2.2.2")
130
+	flags.Set("dns-rm", "3.3.3.3")
131
+	flags.Set("dns-rm", "2.2.2.2")
132
+	// IPv6
133
+	flags.Set("dns-add", "2001:db8:abc8::1")
134
+	// Invalid dns record
135
+	assert.Error(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address")
136
+
137
+	// domains with duplicates
138
+	flags.Set("dns-search-add", "example.com")
139
+	flags.Set("dns-search-add", "example.com")
140
+	flags.Set("dns-search-add", "example.org")
141
+	flags.Set("dns-search-rm", "example.org")
142
+	// Invalid dns search domain
143
+	assert.Error(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain")
144
+
145
+	flags.Set("dns-options-add", "ndots:9")
146
+	flags.Set("dns-options-rm", "timeout:3")
147
+
148
+	config := &swarm.DNSConfig{
149
+		Nameservers: []string{"3.3.3.3", "5.5.5.5"},
150
+		Search:      []string{"localdomain"},
151
+		Options:     []string{"timeout:3"},
152
+	}
153
+
154
+	updateDNSConfig(flags, &config)
155
+
156
+	assert.Equal(t, len(config.Nameservers), 3)
157
+	assert.Equal(t, config.Nameservers[0], "1.1.1.1")
158
+	assert.Equal(t, config.Nameservers[1], "2001:db8:abc8::1")
159
+	assert.Equal(t, config.Nameservers[2], "5.5.5.5")
160
+
161
+	assert.Equal(t, len(config.Search), 2)
162
+	assert.Equal(t, config.Search[0], "example.com")
163
+	assert.Equal(t, config.Search[1], "localdomain")
164
+
165
+	assert.Equal(t, len(config.Options), 1)
166
+	assert.Equal(t, config.Options[0], "ndots:9")
167
+}
168
+
123 169
 func TestUpdateMounts(t *testing.T) {
124 170
 	flags := newUpdateCommand(nil).Flags()
125 171
 	flags.Set("mount-add", "type=volume,source=vol2,target=/toadd")
... ...
@@ -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,
... ...
@@ -169,6 +169,7 @@ This section lists each version from latest to oldest.  Each listing includes a
169 169
 * `GET /info` now returns more structured information about security options.
170 170
 * The `HostConfig` field now includes `CpuCount` that represents the number of CPUs available for execution by the container. Windows daemon only.
171 171
 * `POST /services/create` and `POST /services/(id or name)/update` now accept the `TTY` parameter, which allocate a pseudo-TTY in container.
172
+* `POST /services/create` and `POST /services/(id or name)/update` now accept the `DNSConfig` parameter, which specifies DNS related configurations in resolver configuration file (resolv.conf) through `Nameservers`, `Search`, and `Options`.
172 173
 
173 174
 ### v1.24 API changes
174 175
 
... ...
@@ -5114,7 +5114,12 @@ image](#create-an-image) section for more details.
5114 5114
             }
5115 5115
           ],
5116 5116
           "User": "33",
5117
-          "TTY": false
5117
+          "TTY": false,
5118
+          "DNSConfig": {
5119
+            "Nameservers": ["8.8.8.8"],
5120
+            "Search": ["example.org"],
5121
+            "Options": ["timeout:3"]
5122
+          }
5118 5123
         },
5119 5124
         "LogDriver": {
5120 5125
           "Name": "json-file",
... ...
@@ -5209,6 +5214,11 @@ image](#create-an-image) section for more details.
5209 5209
                   - **Options** - key/value map of driver specific options.
5210 5210
         - **StopGracePeriod** – Amount of time to wait for the container to terminate before
5211 5211
           forcefully killing it.
5212
+        - **DNSConfig** – Specification for DNS related configurations in
5213
+          resolver configuration file (resolv.conf).
5214
+            - **Nameservers** – A list of the IP addresses of the name servers.
5215
+            - **Search** – A search list for host-name lookup.
5216
+            - **Options** – A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
5212 5217
     - **LogDriver** - Log configuration for containers created as part of the
5213 5218
       service.
5214 5219
         - **Name** - Name of the logging driver to use (`json-file`, `syslog`,
... ...
@@ -5394,7 +5404,12 @@ image](#create-an-image) section for more details.
5394 5394
           "Args": [
5395 5395
             "top"
5396 5396
           ],
5397
-          "TTY": true
5397
+          "TTY": true,
5398
+          "DNSConfig": {
5399
+            "Nameservers": ["8.8.8.8"],
5400
+            "Search": ["example.org"],
5401
+            "Options": ["timeout:3"]
5402
+          }
5398 5403
         },
5399 5404
         "Resources": {
5400 5405
           "Limits": {},
... ...
@@ -5460,6 +5475,11 @@ image](#create-an-image) section for more details.
5460 5460
                   - **Options** - key/value map of driver specific options
5461 5461
         - **StopGracePeriod** – Amount of time to wait for the container to terminate before
5462 5462
           forcefully killing it.
5463
+        - **DNSConfig** – Specification for DNS related configurations in
5464
+          resolver configuration file (resolv.conf).
5465
+            - **Nameservers** – A list of the IP addresses of the name servers.
5466
+            - **Search** – A search list for host-name lookup.
5467
+            - **Options** – A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
5463 5468
     - **Resources** – Resource requirements which apply to each individual container created as part
5464 5469
       of the service.
5465 5470
         - **Limits** – Define resources limits.
... ...
@@ -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-options 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 [])
... ...
@@ -26,6 +26,12 @@ Options:
26 26
       --constraint-rm list                 Remove a constraint (default [])
27 27
       --container-label-add list           Add or update a container label (default [])
28 28
       --container-label-rm list            Remove a container label by its key (default [])
29
+      --dns-add list                       Add or update custom DNS servers (default [])
30
+      --dns-options-add list               Add or update DNS options (default [])
31
+      --dns-options-rm list                Remove DNS options (default [])
32
+      --dns-rm list                        Remove custom DNS servers (default [])
33
+      --dns-search-add list                Add or update custom DNS search domains (default [])
34
+      --dns-search-rm list                 Remove DNS search domains (default [])
29 35
       --endpoint-mode string               Endpoint mode (vip or dnsrr)
30 36
       --env-add list                       Add or update an environment variable (default [])
31 37
       --env-rm list                        Remove an environment variable (default [])
... ...
@@ -789,3 +789,49 @@ 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-options=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
+}
819
+
820
+func (s *DockerSwarmSuite) TestDNSConfigUpdate(c *check.C) {
821
+	d := s.AddDaemon(c, true, true)
822
+
823
+	// Create a service
824
+	name := "top"
825
+	_, err := d.Cmd("service", "create", "--name", name, "busybox", "top")
826
+	c.Assert(err, checker.IsNil)
827
+
828
+	// Make sure task has been deployed.
829
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
830
+
831
+	_, err = d.Cmd("service", "update", "--dns-add=1.2.3.4", "--dns-search-add=example.com", "--dns-options-add=timeout:3", name)
832
+	c.Assert(err, checker.IsNil)
833
+
834
+	out, err := d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.DNSConfig }}", name)
835
+	c.Assert(err, checker.IsNil)
836
+	c.Assert(strings.TrimSpace(out), checker.Equals, "{[1.2.3.4] [example.com] [timeout:3]}")
837
+}