From e05f348fa389eb02cfd0b1bc4e678e052467b7ae Mon Sep 17 00:00:00 2001 From: Siew Kam Onn Date: Mon, 25 Aug 2025 12:32:19 +0800 Subject: [PATCH] feat(service): support comma-separated values for host-to-IP mappings --- cli/command/service/update.go | 4 +- cli/command/service/update_test.go | 3 +- docs/reference/commandline/service_update.md | 4 +- opts/opts.go | 45 ++++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 413d1a125b3f..06a0ca920b34 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -95,7 +95,7 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { flags.SetAnnotation(flagDNSOptionAdd, "version", []string{"1.25"}) flags.Var(&options.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") flags.SetAnnotation(flagDNSSearchAdd, "version", []string{"1.25"}) - flags.Var(&options.hosts, flagHostAdd, `Add a custom host-to-IP mapping ("host:ip")`) + flags.Var(opts.NewListOptsCSV(&options.hosts), flagHostAdd, `Add a custom host-to-IP mapping ("host:ip")`) flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) flags.BoolVar(&options.init, flagInit, false, "Use an init inside each service container to forward signals and reap processes") flags.SetAnnotation(flagInit, "version", []string{"1.37"}) @@ -1235,7 +1235,7 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { // Append new hosts (in SwarmKit format) if flags.Changed(flagHostAdd) { - values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetSlice()) + values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(pflag.SliceValue).GetSlice()) newHosts = append(newHosts, values...) } *hosts = removeDuplicates(newHosts) diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 1cc9ab0e8227..74c0183ca061 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -388,6 +388,7 @@ func TestUpdateHealthcheckTable(t *testing.T) { func TestUpdateHosts(t *testing.T) { flags := newUpdateCommand(nil).Flags() + flags.Set("host-add", "a:1.1.1.1,b:2.2.2.2") flags.Set("host-add", "example.net:2.2.2.2") flags.Set("host-add", "ipv6.net:2001:db8:abc8::1") // adding the special "host-gateway" target should work @@ -402,7 +403,7 @@ func TestUpdateHosts(t *testing.T) { assert.ErrorContains(t, flags.Set("host-add", "$example.com$"), `bad format for add-host: "$example.com$"`) hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net", "gateway.docker.internal:host-gateway"} - expected := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2.2.2.2 example.net", "2001:db8:abc8::1 ipv6.net", "host-gateway host.docker.internal"} + expected := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "1.1.1.1 a", "2.2.2.2 b", "2.2.2.2 example.net", "2001:db8:abc8::1 ipv6.net", "host-gateway host.docker.internal"} err := updateHosts(flags, &hosts) assert.NilError(t, err) diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index f3564bcc1f24..9c741da147a5 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -39,8 +39,8 @@ Update a service | `--health-start-interval` | `duration` | | Time between running the check during the start period (ms\|s\|m\|h) | | `--health-start-period` | `duration` | | Start period for the container to initialize before counting retries towards unstable (ms\|s\|m\|h) | | `--health-timeout` | `duration` | | Maximum time to allow one check to run (ms\|s\|m\|h) | -| `--host-add` | `list` | | Add a custom host-to-IP mapping (`host:ip`) | -| `--host-rm` | `list` | | Remove a custom host-to-IP mapping (`host:ip`) | +| `--host-add` | `list` | | Add a custom host-to-IP mapping (`host:ip`). Multiple entries can be separated by comma | +| `--host-rm` | `list` | | Remove a custom host-to-IP mapping (`host:ip`) | | `--hostname` | `string` | | Container hostname | | `--image` | `string` | | Service image tag | | `--init` | `bool` | | Use an init inside each service container to forward signals and reap processes | diff --git a/opts/opts.go b/opts/opts.go index 6ea218130454..ed062f9ee52b 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -125,6 +125,51 @@ func (opts *ListOpts) WithValidator(validator ValidatorFctType) *ListOpts { return opts } +// ListOptsCSV wraps a ListOpts and implements [pflag.SliceValue], allowing +// comma-separated values to be parsed in a single flag instance. +// +// Values passed to Set are split on commas before being validated using the +// wrapped ListOpts' validator (for example, [ValidateExtraHost]). +// +// [pflag.SliceValue]: https://pkg.go.dev/github.com/spf13/pflag#SliceValue +type ListOptsCSV struct{ *ListOpts } + +// NewListOptsCSV returns a new ListOptsCSV using the provided ListOpts. +func NewListOptsCSV(opts *ListOpts) *ListOptsCSV { + return &ListOptsCSV{ListOpts: opts} +} + +// Set splits the value on commas and validates each item using the wrapped +// ListOpts. +func (opts *ListOptsCSV) Set(value string) error { + for _, v := range strings.Split(value, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + if err := opts.ListOpts.Set(v); err != nil { + return err + } + } + return nil +} + +// Append implements pflag.SliceValue. +func (opts *ListOptsCSV) Append(value string) error { + return opts.Set(value) +} + +// Replace implements pflag.SliceValue. +func (opts *ListOptsCSV) Replace(values []string) error { + *opts.ListOpts.values = (*opts.ListOpts.values)[:0] + for _, v := range values { + if err := opts.Set(v); err != nil { + return err + } + } + return nil +} + // MapOpts holds a map of values and a validation function. type MapOpts struct { values map[string]string