Browse code

Add custom DNS settings to service update

This fix adds `--dns-add`, `--dns-rm`, `--dns-opt-add`, `--dns-opt-rm`,
`--dns-search-add` and `--dns-search-rm` to `service update`.

An integration test and a unit test have been added to cover the changes in this fix.

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

Yong Tang authored on 2016/10/27 12:05:39
Showing 8 changed files
... ...
@@ -475,8 +475,14 @@ const (
475 475
 	flagContainerLabelRemove  = "container-label-rm"
476 476
 	flagContainerLabelAdd     = "container-label-add"
477 477
 	flagDNS                   = "dns"
478
-	flagDNSOptions            = "dns-opt"
478
+	flagDNSRemove             = "dns-rm"
479
+	flagDNSAdd                = "dns-add"
480
+	flagDNSOptions            = "dns-options"
481
+	flagDNSOptionsRemove      = "dns-options-rm"
482
+	flagDNSOptionsAdd         = "dns-options-add"
479 483
 	flagDNSSearch             = "dns-search"
484
+	flagDNSSearchRemove       = "dns-search-rm"
485
+	flagDNSSearchAdd          = "dns-search-add"
480 486
 	flagEndpointMode          = "endpoint-mode"
481 487
 	flagHostname              = "hostname"
482 488
 	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")
... ...
@@ -168,6 +168,7 @@ This section lists each version from latest to oldest.  Each listing includes a
168 168
 * `GET /info` now returns more structured information about security options.
169 169
 * The `HostConfig` field now includes `CpuCount` that represents the number of CPUs available for execution by the container. Windows daemon only.
170 170
 * `POST /services/create` and `POST /services/(id or name)/update` now accept the `TTY` parameter, which allocate a pseudo-TTY in container.
171
+* `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`.
171 172
 
172 173
 ### v1.24 API changes
173 174
 
... ...
@@ -5113,7 +5113,12 @@ image](#create-an-image) section for more details.
5113 5113
             }
5114 5114
           ],
5115 5115
           "User": "33",
5116
-          "TTY": false
5116
+          "TTY": false,
5117
+          "DNSConfig": {
5118
+            "Nameservers": ["8.8.8.8"],
5119
+            "Search": ["example.org"],
5120
+            "Options": ["timeout:3"]
5121
+          }
5117 5122
         },
5118 5123
         "LogDriver": {
5119 5124
           "Name": "json-file",
... ...
@@ -5208,6 +5213,11 @@ image](#create-an-image) section for more details.
5208 5208
                   - **Options** - key/value map of driver specific options.
5209 5209
         - **StopGracePeriod** – Amount of time to wait for the container to terminate before
5210 5210
           forcefully killing it.
5211
+        - **DNSConfig** – Specification for DNS related configurations in
5212
+          resolver configuration file (resolv.conf).
5213
+            - **Nameservers** – A list of the IP addresses of the name servers.
5214
+            - **Search** – A search list for host-name lookup.
5215
+            - **Options** – A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
5211 5216
     - **LogDriver** - Log configuration for containers created as part of the
5212 5217
       service.
5213 5218
         - **Name** - Name of the logging driver to use (`json-file`, `syslog`,
... ...
@@ -5393,7 +5403,12 @@ image](#create-an-image) section for more details.
5393 5393
           "Args": [
5394 5394
             "top"
5395 5395
           ],
5396
-          "TTY": true
5396
+          "TTY": true,
5397
+          "DNSConfig": {
5398
+            "Nameservers": ["8.8.8.8"],
5399
+            "Search": ["example.org"],
5400
+            "Options": ["timeout:3"]
5401
+          }
5397 5402
         },
5398 5403
         "Resources": {
5399 5404
           "Limits": {},
... ...
@@ -5459,6 +5474,11 @@ image](#create-an-image) section for more details.
5459 5459
                   - **Options** - key/value map of driver specific options
5460 5460
         - **StopGracePeriod** – Amount of time to wait for the container to terminate before
5461 5461
           forcefully killing it.
5462
+        - **DNSConfig** – Specification for DNS related configurations in
5463
+          resolver configuration file (resolv.conf).
5464
+            - **Nameservers** – A list of the IP addresses of the name servers.
5465
+            - **Search** – A search list for host-name lookup.
5466
+            - **Options** – A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
5462 5467
     - **Resources** – Resource requirements which apply to each individual container created as part
5463 5468
       of the service.
5464 5469
         - **Limits** – Define resources limits.
... ...
@@ -24,7 +24,7 @@ Options:
24 24
       --constraint value                 Placement constraints (default [])
25 25
       --container-label value            Service container labels (default [])
26 26
       --dns list                         Set custom DNS servers (default [])
27
-      --dns-opt list                     Set DNS options (default [])
27
+      --dns-options list                 Set DNS options (default [])
28 28
       --dns-search list                  Set custom DNS search domains (default [])
29 29
       --endpoint-mode string             Endpoint mode (vip or dnsrr)
30 30
   -e, --env value                        Set 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 [])
... ...
@@ -795,7 +795,7 @@ func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) {
795 795
 
796 796
 	// Create a service
797 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")
798
+	_, err := d.Cmd("service", "create", "--name", name, "--dns=1.2.3.4", "--dns-search=example.com", "--dns-options=timeout:3", "busybox", "top")
799 799
 	c.Assert(err, checker.IsNil)
800 800
 
801 801
 	// Make sure task has been deployed.
... ...
@@ -816,3 +816,22 @@ func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) {
816 816
 	c.Assert(out, checker.Contains, expectedOutput2, check.Commentf("Expected '%s', but got %q", expectedOutput2, out))
817 817
 	c.Assert(out, checker.Contains, expectedOutput3, check.Commentf("Expected '%s', but got %q", expectedOutput3, out))
818 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
+}