diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 3efee10f41..17cf19625f 100644 --- a/components/cli/command/service/create.go +++ b/components/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.dnsOption, flagDNSOption, "Set DNS options") 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) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index c48c952e0c..52971ae833 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -397,6 +397,20 @@ func ValidatePort(value string) (string, error) { return value, err } +// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli +// : +// into a swarmkit host format: +// IP_address canonical_hostname [aliases...] +// This assumes input value (:) 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 { name string labels opts.ListOpts @@ -414,6 +428,7 @@ type serviceOptions struct { dns opts.ListOpts dnsSearch opts.ListOpts dnsOption opts.ListOpts + hosts opts.ListOpts resources resourceOptions stopGrace DurationOpt @@ -450,6 +465,7 @@ func newServiceOptions() *serviceOptions { dns: opts.NewListOpts(opts.ValidateIPAddress), dnsOption: opts.NewListOpts(nil), dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), networks: opts.NewListOpts(nil), } } @@ -498,6 +514,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Search: opts.dnsSearch.GetAll(), Options: opts.dnsOption.GetAll(), }, + Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), StopGracePeriod: opts.stopGrace.Value(), Secrets: nil, }, @@ -604,6 +621,9 @@ const ( flagDNSSearchRemove = "dns-search-rm" flagDNSSearchAdd = "dns-search-add" flagEndpointMode = "endpoint-mode" + flagHost = "host" + flagHostAdd = "host-add" + flagHostRemove = "host-rm" flagHostname = "hostname" flagEnv = "env" flagEnvFile = "env-file" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 9741f67d54..f5acc2c511 100644 --- a/components/cli/command/service/update.go +++ b/components/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(), flagDNSOptionRemove, "Remove a DNS option") 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.containerLabels, flagContainerLabelAdd, "Add or update a container label") 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.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") 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 } @@ -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 { return err } @@ -683,6 +691,47 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error 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. // All options will be replaced with those provided on the command line. func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index b99064352a..a3736090ae 100644 --- a/components/cli/command/service/update_test.go +++ b/components/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") +}