feat(service): support comma-separated values for host-to-IP mappings

This commit is contained in:
Siew Kam Onn 2025-08-25 12:32:19 +08:00
parent abe4aa7893
commit e05f348fa3
4 changed files with 51 additions and 5 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 |

View File

@ -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