Browse Source

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

Add custom DNS settings to service definition
Victor Vieux 8 years ago
parent
commit
462e57f05e

+ 15 - 0
api/types/swarm/container.go

@@ -7,6 +7,20 @@ import (
 	"github.com/docker/docker/api/types/mount"
 )
 
+// DNSConfig specifies DNS related configurations in resolver configuration file (resolv.conf)
+// Detailed documentation is available in:
+// http://man7.org/linux/man-pages/man5/resolv.conf.5.html
+// `nameserver`, `search`, `options` have been supported.
+// TODO: `domain` is not supported yet.
+type DNSConfig struct {
+	// Nameservers specifies the IP addresses of the name servers
+	Nameservers []string `json:",omitempty"`
+	// Search specifies the search list for host-name lookup
+	Search []string `json:",omitempty"`
+	// Options allows certain internal resolver variables to be modified
+	Options []string `json:",omitempty"`
+}
+
 // ContainerSpec represents the spec of a container.
 type ContainerSpec struct {
 	Image           string                  `json:",omitempty"`
@@ -22,4 +36,5 @@ type ContainerSpec struct {
 	Mounts          []mount.Mount           `json:",omitempty"`
 	StopGracePeriod *time.Duration          `json:",omitempty"`
 	Healthcheck     *container.HealthConfig `json:",omitempty"`
+	DNSConfig       *DNSConfig              `json:",omitempty"`
 }

+ 3 - 0
cli/command/service/create.go

@@ -41,6 +41,9 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
 	flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
 	flags.StringSliceVar(&opts.groups, flagGroup, []string{}, "Set one or more supplementary user groups for the container")
+	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
+	flags.Var(&opts.dnsOptions, flagDNSOptions, "Set DNS options")
+	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
 
 	flags.SetInterspersed(false)
 	return cmd

+ 31 - 11
cli/command/service/opts.go

@@ -296,6 +296,9 @@ type serviceOptions struct {
 	groups          []string
 	tty             bool
 	mounts          opts.MountOpt
+	dns             opts.ListOpts
+	dnsSearch       opts.ListOpts
+	dnsOptions      opts.ListOpts
 
 	resources resourceOptions
 	stopGrace DurationOpt
@@ -325,7 +328,10 @@ func newServiceOptions() *serviceOptions {
 		endpoint: endpointOptions{
 			ports: opts.NewListOpts(ValidatePort),
 		},
-		logDriver: newLogDriverOptions(),
+		logDriver:  newLogDriverOptions(),
+		dns:        opts.NewListOpts(opts.ValidateIPAddress),
+		dnsOptions: opts.NewListOpts(nil),
+		dnsSearch:  opts.NewListOpts(opts.ValidateDNSSearch),
 	}
 }
 
@@ -358,16 +364,21 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
 		},
 		TaskTemplate: swarm.TaskSpec{
 			ContainerSpec: swarm.ContainerSpec{
-				Image:           opts.image,
-				Args:            opts.args,
-				Env:             currentEnv,
-				Hostname:        opts.hostname,
-				Labels:          runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
-				Dir:             opts.workdir,
-				User:            opts.user,
-				Groups:          opts.groups,
-				TTY:             opts.tty,
-				Mounts:          opts.mounts.Value(),
+				Image:    opts.image,
+				Args:     opts.args,
+				Env:      currentEnv,
+				Hostname: opts.hostname,
+				Labels:   runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
+				Dir:      opts.workdir,
+				User:     opts.user,
+				Groups:   opts.groups,
+				TTY:      opts.tty,
+				Mounts:   opts.mounts.Value(),
+				DNSConfig: &swarm.DNSConfig{
+					Nameservers: opts.dns.GetAll(),
+					Search:      opts.dnsSearch.GetAll(),
+					Options:     opts.dnsOptions.GetAll(),
+				},
 				StopGracePeriod: opts.stopGrace.Value(),
 			},
 			Networks:      convertNetworks(opts.networks),
@@ -463,6 +474,15 @@ const (
 	flagContainerLabel        = "container-label"
 	flagContainerLabelRemove  = "container-label-rm"
 	flagContainerLabelAdd     = "container-label-add"
+	flagDNS                   = "dns"
+	flagDNSRemove             = "dns-rm"
+	flagDNSAdd                = "dns-add"
+	flagDNSOptions            = "dns-options"
+	flagDNSOptionsRemove      = "dns-options-rm"
+	flagDNSOptionsAdd         = "dns-options-add"
+	flagDNSSearch             = "dns-search"
+	flagDNSSearchRemove       = "dns-search-rm"
+	flagDNSSearchAdd          = "dns-search-add"
 	flagEndpointMode          = "endpoint-mode"
 	flagHostname              = "hostname"
 	flagEnv                   = "env"

+ 81 - 0
cli/command/service/update.go

@@ -48,6 +48,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
 	flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port")
 	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
+	flags.Var(newListOptsVar(), flagDNSRemove, "Remove custom DNS servers")
+	flags.Var(newListOptsVar(), flagDNSOptionsRemove, "Remove DNS options")
+	flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove DNS search domains")
 	flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label")
 	flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
 	flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
@@ -55,6 +58,10 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update a placement constraint")
 	flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
 	flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add an additional supplementary user group to the container")
+	flags.Var(&opts.dns, flagDNSAdd, "Add or update custom DNS servers")
+	flags.Var(&opts.dnsOptions, flagDNSOptionsAdd, "Add or update DNS options")
+	flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update custom DNS search domains")
+
 	return cmd
 }
 
@@ -257,6 +264,15 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
 		}
 	}
 
+	if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionsAdd, flagDNSOptionsRemove, flagDNSSearchAdd, flagDNSSearchRemove) {
+		if cspec.DNSConfig == nil {
+			cspec.DNSConfig = &swarm.DNSConfig{}
+		}
+		if err := updateDNSConfig(flags, &cspec.DNSConfig); err != nil {
+			return err
+		}
+	}
+
 	if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
 		return err
 	}
@@ -484,6 +500,71 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error {
 	return nil
 }
 
+func removeDuplicates(entries []string) []string {
+	hit := map[string]bool{}
+	newEntries := []string{}
+	for _, v := range entries {
+		if !hit[v] {
+			newEntries = append(newEntries, v)
+			hit[v] = true
+		}
+	}
+	return newEntries
+}
+
+func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
+	newConfig := &swarm.DNSConfig{}
+
+	nameservers := (*config).Nameservers
+	if flags.Changed(flagDNSAdd) {
+		values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetAll()
+		nameservers = append(nameservers, values...)
+	}
+	nameservers = removeDuplicates(nameservers)
+	toRemove := buildToRemoveSet(flags, flagDNSRemove)
+	for _, nameserver := range nameservers {
+		if _, exists := toRemove[nameserver]; !exists {
+			newConfig.Nameservers = append(newConfig.Nameservers, nameserver)
+
+		}
+	}
+	// Sort so that result is predictable.
+	sort.Strings(newConfig.Nameservers)
+
+	search := (*config).Search
+	if flags.Changed(flagDNSSearchAdd) {
+		values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetAll()
+		search = append(search, values...)
+	}
+	search = removeDuplicates(search)
+	toRemove = buildToRemoveSet(flags, flagDNSSearchRemove)
+	for _, entry := range search {
+		if _, exists := toRemove[entry]; !exists {
+			newConfig.Search = append(newConfig.Search, entry)
+		}
+	}
+	// Sort so that result is predictable.
+	sort.Strings(newConfig.Search)
+
+	options := (*config).Options
+	if flags.Changed(flagDNSOptionsAdd) {
+		values := flags.Lookup(flagDNSOptionsAdd).Value.(*opts.ListOpts).GetAll()
+		options = append(options, values...)
+	}
+	options = removeDuplicates(options)
+	toRemove = buildToRemoveSet(flags, flagDNSOptionsRemove)
+	for _, option := range options {
+		if _, exists := toRemove[option]; !exists {
+			newConfig.Options = append(newConfig.Options, option)
+		}
+	}
+	// Sort so that result is predictable.
+	sort.Strings(newConfig.Options)
+
+	*config = newConfig
+	return nil
+}
+
 type byPortConfig []swarm.PortConfig
 
 func (r byPortConfig) Len() int      { return len(r) }

+ 46 - 0
cli/command/service/update_test.go

@@ -120,6 +120,52 @@ func TestUpdateGroups(t *testing.T) {
 	assert.Equal(t, groups[2], "wheel")
 }
 
+func TestUpdateDNSConfig(t *testing.T) {
+	flags := newUpdateCommand(nil).Flags()
+
+	// IPv4, with duplicates
+	flags.Set("dns-add", "1.1.1.1")
+	flags.Set("dns-add", "1.1.1.1")
+	flags.Set("dns-add", "2.2.2.2")
+	flags.Set("dns-rm", "3.3.3.3")
+	flags.Set("dns-rm", "2.2.2.2")
+	// IPv6
+	flags.Set("dns-add", "2001:db8:abc8::1")
+	// Invalid dns record
+	assert.Error(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address")
+
+	// domains with duplicates
+	flags.Set("dns-search-add", "example.com")
+	flags.Set("dns-search-add", "example.com")
+	flags.Set("dns-search-add", "example.org")
+	flags.Set("dns-search-rm", "example.org")
+	// Invalid dns search domain
+	assert.Error(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain")
+
+	flags.Set("dns-options-add", "ndots:9")
+	flags.Set("dns-options-rm", "timeout:3")
+
+	config := &swarm.DNSConfig{
+		Nameservers: []string{"3.3.3.3", "5.5.5.5"},
+		Search:      []string{"localdomain"},
+		Options:     []string{"timeout:3"},
+	}
+
+	updateDNSConfig(flags, &config)
+
+	assert.Equal(t, len(config.Nameservers), 3)
+	assert.Equal(t, config.Nameservers[0], "1.1.1.1")
+	assert.Equal(t, config.Nameservers[1], "2001:db8:abc8::1")
+	assert.Equal(t, config.Nameservers[2], "5.5.5.5")
+
+	assert.Equal(t, len(config.Search), 2)
+	assert.Equal(t, config.Search[0], "example.com")
+	assert.Equal(t, config.Search[1], "localdomain")
+
+	assert.Equal(t, len(config.Options), 1)
+	assert.Equal(t, config.Options[0], "ndots:9")
+}
+
 func TestUpdateMounts(t *testing.T) {
 	flags := newUpdateCommand(nil).Flags()
 	flags.Set("mount-add", "type=volume,source=vol2,target=/toadd")

+ 16 - 0
daemon/cluster/convert/container.go

@@ -25,6 +25,14 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
 		TTY:      c.TTY,
 	}
 
+	if c.DNSConfig != nil {
+		containerSpec.DNSConfig = &types.DNSConfig{
+			Nameservers: c.DNSConfig.Nameservers,
+			Search:      c.DNSConfig.Search,
+			Options:     c.DNSConfig.Options,
+		}
+	}
+
 	// Mounts
 	for _, m := range c.Mounts {
 		mount := mounttypes.Mount{
@@ -81,6 +89,14 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		TTY:      c.TTY,
 	}
 
+	if c.DNSConfig != nil {
+		containerSpec.DNSConfig = &swarmapi.ContainerSpec_DNSConfig{
+			Nameservers: c.DNSConfig.Nameservers,
+			Search:      c.DNSConfig.Search,
+			Options:     c.DNSConfig.Options,
+		}
+	}
+
 	if c.StopGracePeriod != nil {
 		containerSpec.StopGracePeriod = ptypes.DurationProto(*c.StopGracePeriod)
 	}

+ 6 - 0
daemon/cluster/executor/container/container.go

@@ -327,6 +327,12 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
 		GroupAdd:  c.spec().Groups,
 	}
 
+	if c.spec().DNSConfig != nil {
+		hc.DNS = c.spec().DNSConfig.Nameservers
+		hc.DNSSearch = c.spec().DNSConfig.Search
+		hc.DNSOptions = c.spec().DNSConfig.Options
+	}
+
 	if c.task.LogDriver != nil {
 		hc.LogConfig = enginecontainer.LogConfig{
 			Type:   c.task.LogDriver.Name,

+ 1 - 0
docs/reference/api/docker_remote_api.md

@@ -169,6 +169,7 @@ This section lists each version from latest to oldest.  Each listing includes a
 * `GET /info` now returns more structured information about security options.
 * The `HostConfig` field now includes `CpuCount` that represents the number of CPUs available for execution by the container. Windows daemon only.
 * `POST /services/create` and `POST /services/(id or name)/update` now accept the `TTY` parameter, which allocate a pseudo-TTY in container.
+* `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`.
 
 ### v1.24 API changes
 

+ 22 - 2
docs/reference/api/docker_remote_api_v1.25.md

@@ -5114,7 +5114,12 @@ image](#create-an-image) section for more details.
             }
           ],
           "User": "33",
-          "TTY": false
+          "TTY": false,
+          "DNSConfig": {
+            "Nameservers": ["8.8.8.8"],
+            "Search": ["example.org"],
+            "Options": ["timeout:3"]
+          }
         },
         "LogDriver": {
           "Name": "json-file",
@@ -5209,6 +5214,11 @@ image](#create-an-image) section for more details.
                   - **Options** - key/value map of driver specific options.
         - **StopGracePeriod** – Amount of time to wait for the container to terminate before
           forcefully killing it.
+        - **DNSConfig** – Specification for DNS related configurations in
+          resolver configuration file (resolv.conf).
+            - **Nameservers** – A list of the IP addresses of the name servers.
+            - **Search** – A search list for host-name lookup.
+            - **Options** – A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
     - **LogDriver** - Log configuration for containers created as part of the
       service.
         - **Name** - Name of the logging driver to use (`json-file`, `syslog`,
@@ -5394,7 +5404,12 @@ image](#create-an-image) section for more details.
           "Args": [
             "top"
           ],
-          "TTY": true
+          "TTY": true,
+          "DNSConfig": {
+            "Nameservers": ["8.8.8.8"],
+            "Search": ["example.org"],
+            "Options": ["timeout:3"]
+          }
         },
         "Resources": {
           "Limits": {},
@@ -5460,6 +5475,11 @@ image](#create-an-image) section for more details.
                   - **Options** - key/value map of driver specific options
         - **StopGracePeriod** – Amount of time to wait for the container to terminate before
           forcefully killing it.
+        - **DNSConfig** – Specification for DNS related configurations in
+          resolver configuration file (resolv.conf).
+            - **Nameservers** – A list of the IP addresses of the name servers.
+            - **Search** – A search list for host-name lookup.
+            - **Options** – A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
     - **Resources** – Resource requirements which apply to each individual container created as part
       of the service.
         - **Limits** – Define resources limits.

+ 3 - 0
docs/reference/commandline/service_create.md

@@ -23,6 +23,9 @@ Create a new service
 Options:
       --constraint value                 Placement constraints (default [])
       --container-label value            Service container labels (default [])
+      --dns list                         Set custom DNS servers (default [])
+      --dns-options list                 Set DNS options (default [])
+      --dns-search list                  Set custom DNS search domains (default [])
       --endpoint-mode string             Endpoint mode (vip or dnsrr)
   -e, --env value                        Set environment variables (default [])
       --env-file value                   Read in a file of environment variables (default [])

+ 6 - 0
docs/reference/commandline/service_update.md

@@ -26,6 +26,12 @@ Options:
       --constraint-rm list                 Remove a constraint (default [])
       --container-label-add list           Add or update a container label (default [])
       --container-label-rm list            Remove a container label by its key (default [])
+      --dns-add list                       Add or update custom DNS servers (default [])
+      --dns-options-add list               Add or update DNS options (default [])
+      --dns-options-rm list                Remove DNS options (default [])
+      --dns-rm list                        Remove custom DNS servers (default [])
+      --dns-search-add list                Add or update custom DNS search domains (default [])
+      --dns-search-rm list                 Remove DNS search domains (default [])
       --endpoint-mode string               Endpoint mode (vip or dnsrr)
       --env-add list                       Add or update an environment variable (default [])
       --env-rm list                        Remove an environment variable (default [])

+ 46 - 0
integration-cli/docker_cli_swarm_test.go

@@ -789,3 +789,49 @@ func (s *DockerSwarmSuite) TestSwarmServiceTTYUpdate(c *check.C) {
 	c.Assert(err, checker.IsNil)
 	c.Assert(strings.TrimSpace(out), checker.Equals, "true")
 }
+
+func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	// Create a service
+	name := "top"
+	_, err := d.Cmd("service", "create", "--name", name, "--dns=1.2.3.4", "--dns-search=example.com", "--dns-options=timeout:3", "busybox", "top")
+	c.Assert(err, checker.IsNil)
+
+	// Make sure task has been deployed.
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
+
+	// We need to get the container id.
+	out, err := d.Cmd("ps", "-a", "-q", "--no-trunc")
+	c.Assert(err, checker.IsNil)
+	id := strings.TrimSpace(out)
+
+	// Compare against expected output.
+	expectedOutput1 := "nameserver 1.2.3.4"
+	expectedOutput2 := "search example.com"
+	expectedOutput3 := "options timeout:3"
+	out, err = d.Cmd("exec", id, "cat", "/etc/resolv.conf")
+	c.Assert(err, checker.IsNil)
+	c.Assert(out, checker.Contains, expectedOutput1, check.Commentf("Expected '%s', but got %q", expectedOutput1, out))
+	c.Assert(out, checker.Contains, expectedOutput2, check.Commentf("Expected '%s', but got %q", expectedOutput2, out))
+	c.Assert(out, checker.Contains, expectedOutput3, check.Commentf("Expected '%s', but got %q", expectedOutput3, out))
+}
+
+func (s *DockerSwarmSuite) TestDNSConfigUpdate(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	// Create a service
+	name := "top"
+	_, err := d.Cmd("service", "create", "--name", name, "busybox", "top")
+	c.Assert(err, checker.IsNil)
+
+	// Make sure task has been deployed.
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
+
+	_, err = d.Cmd("service", "update", "--dns-add=1.2.3.4", "--dns-search-add=example.com", "--dns-options-add=timeout:3", name)
+	c.Assert(err, checker.IsNil)
+
+	out, err := d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.DNSConfig }}", name)
+	c.Assert(err, checker.IsNil)
+	c.Assert(strings.TrimSpace(out), checker.Equals, "{[1.2.3.4] [example.com] [timeout:3]}")
+}