浏览代码

Merge pull request #28031 from yongtang/27902-extra-hosts

Add flag `--host` to `service create` and `--host-add/rm` to `service update`
Victor Vieux 8 年之前
父节点
当前提交
bed96ce922

+ 6 - 2
api/types/swarm/container.go

@@ -36,6 +36,10 @@ type ContainerSpec struct {
 	Mounts          []mount.Mount           `json:",omitempty"`
 	Mounts          []mount.Mount           `json:",omitempty"`
 	StopGracePeriod *time.Duration          `json:",omitempty"`
 	StopGracePeriod *time.Duration          `json:",omitempty"`
 	Healthcheck     *container.HealthConfig `json:",omitempty"`
 	Healthcheck     *container.HealthConfig `json:",omitempty"`
-	DNSConfig       *DNSConfig              `json:",omitempty"`
-	Secrets         []*SecretReference      `json:",omitempty"`
+	// The format of extra hosts on swarmkit is specified in:
+	// http://man7.org/linux/man-pages/man5/hosts.5.html
+	//    IP_address canonical_hostname [aliases...]
+	Hosts     []string           `json:",omitempty"`
+	DNSConfig *DNSConfig         `json:",omitempty"`
+	Secrets   []*SecretReference `json:",omitempty"`
 }
 }

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

@@ -45,6 +45,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
 	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
 	flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options")
 	flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options")
 	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
 	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
+	flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
 
 
 	flags.SetInterspersed(false)
 	flags.SetInterspersed(false)
 	return cmd
 	return cmd

+ 20 - 0
cli/command/service/opts.go

@@ -397,6 +397,20 @@ func ValidatePort(value string) (string, error) {
 	return value, err
 	return value, err
 }
 }
 
 
+// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli
+//     <host>:<ip>
+// into a swarmkit host format:
+//     IP_address canonical_hostname [aliases...]
+// This assumes input value (<host>:<ip>) has already been validated
+func convertExtraHostsToSwarmHosts(extraHosts []string) []string {
+	hosts := []string{}
+	for _, extraHost := range extraHosts {
+		parts := strings.SplitN(extraHost, ":", 2)
+		hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0]))
+	}
+	return hosts
+}
+
 type serviceOptions struct {
 type serviceOptions struct {
 	name            string
 	name            string
 	labels          opts.ListOpts
 	labels          opts.ListOpts
@@ -414,6 +428,7 @@ type serviceOptions struct {
 	dns             opts.ListOpts
 	dns             opts.ListOpts
 	dnsSearch       opts.ListOpts
 	dnsSearch       opts.ListOpts
 	dnsOption       opts.ListOpts
 	dnsOption       opts.ListOpts
+	hosts           opts.ListOpts
 
 
 	resources resourceOptions
 	resources resourceOptions
 	stopGrace DurationOpt
 	stopGrace DurationOpt
@@ -450,6 +465,7 @@ func newServiceOptions() *serviceOptions {
 		dns:       opts.NewListOpts(opts.ValidateIPAddress),
 		dns:       opts.NewListOpts(opts.ValidateIPAddress),
 		dnsOption: opts.NewListOpts(nil),
 		dnsOption: opts.NewListOpts(nil),
 		dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
 		dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
+		hosts:     opts.NewListOpts(runconfigopts.ValidateExtraHost),
 		networks:  opts.NewListOpts(nil),
 		networks:  opts.NewListOpts(nil),
 	}
 	}
 }
 }
@@ -498,6 +514,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
 					Search:      opts.dnsSearch.GetAll(),
 					Search:      opts.dnsSearch.GetAll(),
 					Options:     opts.dnsOption.GetAll(),
 					Options:     opts.dnsOption.GetAll(),
 				},
 				},
+				Hosts:           convertExtraHostsToSwarmHosts(opts.hosts.GetAll()),
 				StopGracePeriod: opts.stopGrace.Value(),
 				StopGracePeriod: opts.stopGrace.Value(),
 				Secrets:         nil,
 				Secrets:         nil,
 			},
 			},
@@ -604,6 +621,9 @@ const (
 	flagDNSSearchRemove       = "dns-search-rm"
 	flagDNSSearchRemove       = "dns-search-rm"
 	flagDNSSearchAdd          = "dns-search-add"
 	flagDNSSearchAdd          = "dns-search-add"
 	flagEndpointMode          = "endpoint-mode"
 	flagEndpointMode          = "endpoint-mode"
+	flagHost                  = "host"
+	flagHostAdd               = "host-add"
+	flagHostRemove            = "host-rm"
 	flagHostname              = "hostname"
 	flagHostname              = "hostname"
 	flagEnv                   = "env"
 	flagEnv                   = "env"
 	flagEnvFile               = "env-file"
 	flagEnvFile               = "env-file"

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

@@ -52,6 +52,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server")
 	flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server")
 	flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option")
 	flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option")
 	flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain")
 	flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain")
+	flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)")
 	flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label")
 	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.containerLabels, flagContainerLabelAdd, "Add or update a container label")
 	flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
 	flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
@@ -64,6 +65,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server")
 	flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server")
 	flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option")
 	flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option")
 	flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain")
 	flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain")
+	flags.Var(&opts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)")
 
 
 	return cmd
 	return cmd
 }
 }
@@ -283,6 +285,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
 		}
 		}
 	}
 	}
 
 
+	if anyChanged(flags, flagHostAdd, flagHostRemove) {
+		if err := updateHosts(flags, &cspec.Hosts); err != nil {
+			return err
+		}
+	}
+
 	if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
 	if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
 		return err
 		return err
 	}
 	}
@@ -683,6 +691,47 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error
 	return nil
 	return nil
 }
 }
 
 
+func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
+	// Combine existing Hosts (in swarmkit format) with the host to add (convert to swarmkit format)
+	if flags.Changed(flagHostAdd) {
+		values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetAll())
+		*hosts = append(*hosts, values...)
+	}
+	// Remove duplicate
+	*hosts = removeDuplicates(*hosts)
+
+	keysToRemove := make(map[string]struct{})
+	if flags.Changed(flagHostRemove) {
+		var empty struct{}
+		extraHostsToRemove := flags.Lookup(flagHostRemove).Value.(*opts.ListOpts).GetAll()
+		for _, entry := range extraHostsToRemove {
+			key := strings.SplitN(entry, ":", 2)[0]
+			keysToRemove[key] = empty
+		}
+	}
+
+	newHosts := []string{}
+	for _, entry := range *hosts {
+		// Since this is in swarmkit format, we need to find the key, which is canonical_hostname of:
+		// IP_address canonical_hostname [aliases...]
+		parts := strings.Fields(entry)
+		if len(parts) > 1 {
+			key := parts[1]
+			if _, exists := keysToRemove[key]; !exists {
+				newHosts = append(newHosts, entry)
+			}
+		} else {
+			newHosts = append(newHosts, entry)
+		}
+	}
+
+	// Sort so that result is predictable.
+	sort.Strings(newHosts)
+
+	*hosts = newHosts
+	return nil
+}
+
 // updateLogDriver updates the log driver only if the log driver flag is set.
 // updateLogDriver updates the log driver only if the log driver flag is set.
 // All options will be replaced with those provided on the command line.
 // All options will be replaced with those provided on the command line.
 func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
 func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {

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

@@ -339,3 +339,23 @@ func TestUpdateHealthcheckTable(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
+
+func TestUpdateHosts(t *testing.T) {
+	flags := newUpdateCommand(nil).Flags()
+	flags.Set("host-add", "example.net:2.2.2.2")
+	flags.Set("host-add", "ipv6.net:2001:db8:abc8::1")
+	// remove with ipv6 should work
+	flags.Set("host-rm", "example.net:2001:db8:abc8::1")
+	// just hostname should work as well
+	flags.Set("host-rm", "example.net")
+	// bad format error
+	assert.Error(t, flags.Set("host-add", "$example.com$"), "bad format for add-host:")
+
+	hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"}
+
+	updateHosts(flags, &hosts)
+	assert.Equal(t, len(hosts), 3)
+	assert.Equal(t, hosts[0], "1.2.3.4 example.com")
+	assert.Equal(t, hosts[1], "2001:db8:abc8::1 ipv6.net")
+	assert.Equal(t, hosts[2], "4.3.2.1 example.org")
+}

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

@@ -24,6 +24,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
 		User:     c.User,
 		User:     c.User,
 		Groups:   c.Groups,
 		Groups:   c.Groups,
 		TTY:      c.TTY,
 		TTY:      c.TTY,
+		Hosts:    c.Hosts,
 		Secrets:  secretReferencesFromGRPC(c.Secrets),
 		Secrets:  secretReferencesFromGRPC(c.Secrets),
 	}
 	}
 
 
@@ -132,6 +133,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		User:     c.User,
 		User:     c.User,
 		Groups:   c.Groups,
 		Groups:   c.Groups,
 		TTY:      c.TTY,
 		TTY:      c.TTY,
+		Hosts:    c.Hosts,
 		Secrets:  secretReferencesToGRPC(c.Secrets),
 		Secrets:  secretReferencesToGRPC(c.Secrets),
 	}
 	}
 
 

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

@@ -345,6 +345,20 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
 		hc.DNSOptions = c.spec().DNSConfig.Options
 		hc.DNSOptions = c.spec().DNSConfig.Options
 	}
 	}
 
 
+	// The format of extra hosts on swarmkit is specified in:
+	// http://man7.org/linux/man-pages/man5/hosts.5.html
+	//    IP_address canonical_hostname [aliases...]
+	// However, the format of ExtraHosts in HostConfig is
+	//    <host>:<ip>
+	// We need to do the conversion here
+	// (Alias is ignored for now)
+	for _, entry := range c.spec().Hosts {
+		parts := strings.Fields(entry)
+		if len(parts) > 1 {
+			hc.ExtraHosts = append(hc.ExtraHosts, fmt.Sprintf("%s:%s", parts[1], parts[0]))
+		}
+	}
+
 	if c.task.LogDriver != nil {
 	if c.task.LogDriver != nil {
 		hc.LogConfig = enginecontainer.LogConfig{
 		hc.LogConfig = enginecontainer.LogConfig{
 			Type:   c.task.LogDriver.Name,
 			Type:   c.task.LogDriver.Name,

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

@@ -35,6 +35,7 @@ Options:
       --health-retries int               Consecutive failures needed to report unhealthy
       --health-retries int               Consecutive failures needed to report unhealthy
       --health-timeout duration          Maximum time to allow one check to run (default none)
       --health-timeout duration          Maximum time to allow one check to run (default none)
       --help                             Print usage
       --help                             Print usage
+      --host list                        Set one or more custom host-to-IP mappings (host:ip) (default [])
       --hostname string                  Container hostname
       --hostname string                  Container hostname
   -l, --label list                       Service labels (default [])
   -l, --label list                       Service labels (default [])
       --limit-cpu decimal                Limit CPUs (default 0.000)
       --limit-cpu decimal                Limit CPUs (default 0.000)

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

@@ -43,6 +43,8 @@ Options:
       --health-retries int               Consecutive failures needed to report unhealthy
       --health-retries int               Consecutive failures needed to report unhealthy
       --health-timeout duration          Maximum time to allow one check to run (default none)
       --health-timeout duration          Maximum time to allow one check to run (default none)
       --help                             Print usage
       --help                             Print usage
+      --host-add list                    Add or update a custom host-to-IP mapping (host:ip) (default [])
+      --host-rm list                     Remove a custom host-to-IP mapping (host:ip) (default [])
       --image string                     Service image tag
       --image string                     Service image tag
       --label-add list                   Add or update a service label (default [])
       --label-add list                   Add or update a service label (default [])
       --label-rm list                    Remove a label by its key (default [])
       --label-rm list                    Remove a label by its key (default [])

+ 23 - 0
integration-cli/docker_cli_swarm_test.go

@@ -1031,3 +1031,26 @@ func (s *DockerSwarmSuite) TestSwarmRotateUnlockKey(c *check.C) {
 		unlockKey = newUnlockKey
 		unlockKey = newUnlockKey
 	}
 	}
 }
 }
+
+func (s *DockerSwarmSuite) TestExtraHosts(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	// Create a service
+	name := "top"
+	_, err := d.Cmd("service", "create", "--name", name, "--host=example.com:1.2.3.4", "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.
+	expectedOutput := "1.2.3.4\texample.com"
+	out, err = d.Cmd("exec", id, "cat", "/etc/hosts")
+	c.Assert(err, checker.IsNil)
+	c.Assert(out, checker.Contains, expectedOutput, check.Commentf("Expected '%s', but got %q", expectedOutput, out))
+}