mirror of
https://github.com/moby/moby.git
synced 2025-08-01 05:47:11 +03:00
Add flag --host
to service create
and --host-add/--host-rm
to service update
This fix tries to address 27902 by adding a flag `--host` to `docker service create` and `--host-add/--host-rm` to `docker service update`, so that it is possible to specify extra `host:ip` settings in `/etc/hosts`. This fix adds `Hosts` in swarmkit's `ContainerSpec` so that it is possible to specify extra hosts during service creation. Related docs has been updated. An integration test has been added. This fix fixes 27902. Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
@ -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"`
|
||||||
|
// 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"`
|
DNSConfig *DNSConfig `json:",omitempty"`
|
||||||
Secrets []*SecretReference `json:",omitempty"`
|
Secrets []*SecretReference `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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 [])
|
||||||
|
@ -1028,3 +1028,26 @@ func (s *DockerSwarmSuite) TestSwarmRotateUnlockKey(c *check.C) {
|
|||||||
c.Assert(outs, checker.Not(checker.Contains), "Swarm is encrypted and needs to be unlocked")
|
c.Assert(outs, checker.Not(checker.Contains), "Swarm is encrypted and needs to be unlocked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user