diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index fe5fef5bae..e1ab81c5f9 100644 --- a/api/types/swarm/container.go +++ b/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"` } diff --git a/cli/command/service/create.go b/cli/command/service/create.go index e2c4c4d116..6aca4635ae 100644 --- a/cli/command/service/create.go +++ b/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 diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 989fd18b8f..7a5db67b79 100644 --- a/cli/command/service/opts.go +++ b/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" diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 32a23f23ff..d3088720a0 100644 --- a/cli/command/service/update.go +++ b/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) } diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 2123d1b794..91829b8615 100644 --- a/cli/command/service/update_test.go +++ b/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") diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 0ce4fa371f..38749dd8b2 100644 --- a/daemon/cluster/convert/container.go +++ b/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) } diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 68dfce7dcd..5598c6b1d4 100644 --- a/daemon/cluster/executor/container/container.go +++ b/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, diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 2edaaa7d26..3548deac5e 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/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 diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index d7d3c121e1..15340ad6be 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/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. diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 3156e41064..4d95ce96a2 100644 --- a/docs/reference/commandline/service_create.md +++ b/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 []) diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index 212b427f03..531c0009dc 100644 --- a/docs/reference/commandline/service_update.md +++ b/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 []) diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index 6bb4e634b4..bee60fd29f 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/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]}") +}