docker stack: allow '=' separator in extra_hosts

extra_hosts in the compose file format allows '=' as a separator, and brackets
around IP addresses, the engine API doesn't.

So, transform the values when reading a compose file for 'docker stack'.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray 2024-02-07 14:55:01 +00:00
parent 79fa65e7b5
commit c986d09bca
2 changed files with 54 additions and 13 deletions

View File

@ -328,7 +328,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false), reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false), reflect.TypeOf(types.HostsList{}): transformHostsList,
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
reflect.TypeOf(types.Duration(0)): transformStringToDuration, reflect.TypeOf(types.Duration(0)): transformStringToDuration,
@ -808,28 +808,58 @@ var transformStringList TransformerFunc = func(data any) (any, error) {
} }
} }
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc { var transformHostsList TransformerFunc = func(data any) (any, error) {
return func(data any) (any, error) { hl := transformListOrMapping(data, ":", false, []string{"=", ":"})
return transformMappingOrList(data, sep, allowNil), nil
// Remove brackets from IP addresses if present (for example "[::1]" -> "::1").
result := make([]string, 0, len(hl))
for _, hip := range hl {
host, ip, _ := strings.Cut(hip, ":")
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
ip = ip[1 : len(ip)-1]
}
result = append(result, fmt.Sprintf("%s:%s", host, ip))
} }
return result, nil
} }
func transformListOrMappingFunc(sep string, allowNil bool) TransformerFunc { // transformListOrMapping transforms pairs of strings that may be represented as
return func(data any) (any, error) { // a map, or a list of '=' or ':' separated strings, into a list of ':' separated
return transformListOrMapping(data, sep, allowNil), nil // strings.
} func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowSeps []string) []string {
}
func transformListOrMapping(listOrMapping any, sep string, allowNil bool) any {
switch value := listOrMapping.(type) { switch value := listOrMapping.(type) {
case map[string]any: case map[string]any:
return toStringList(value, sep, allowNil) return toStringList(value, sep, allowNil)
case []any: case []any:
return listOrMapping result := make([]string, 0, len(value))
for _, entry := range value {
for i, allowSep := range allowSeps {
entry := fmt.Sprint(entry)
k, v, ok := strings.Cut(entry, allowSep)
if ok {
// Entry uses this allowed separator. Add it to the result, using
// sep as a separator.
result = append(result, fmt.Sprintf("%s%s%s", k, sep, v))
break
} else if i == len(allowSeps)-1 {
// No more separators to try, keep the entry if allowNil.
if allowNil {
result = append(result, k)
}
}
}
}
return result
} }
panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
} }
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
return func(data any) (any, error) {
return transformMappingOrList(data, sep, allowNil), nil
}
}
func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any { func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any {
switch values := mappingOrList.(type) { switch values := mappingOrList.(type) {
case map[string]any: case map[string]any:

View File

@ -1302,12 +1302,14 @@ services:
extra_hosts: extra_hosts:
"zulu": "162.242.195.82" "zulu": "162.242.195.82"
"alpha": "50.31.209.229" "alpha": "50.31.209.229"
"beta": "[fd20:f8a7:6e5b::2]"
"host.docker.internal": "host-gateway" "host.docker.internal": "host-gateway"
`) `)
assert.NilError(t, err) assert.NilError(t, err)
expected := types.HostsList{ expected := types.HostsList{
"alpha:50.31.209.229", "alpha:50.31.209.229",
"beta:fd20:f8a7:6e5b::2",
"host.docker.internal:host-gateway", "host.docker.internal:host-gateway",
"zulu:162.242.195.82", "zulu:162.242.195.82",
} }
@ -1324,16 +1326,25 @@ services:
image: busybox image: busybox
extra_hosts: extra_hosts:
- "zulu:162.242.195.82" - "zulu:162.242.195.82"
- "whiskey=162.242.195.83"
- "alpha:50.31.209.229" - "alpha:50.31.209.229"
- "zulu:ff02::1" - "zulu:ff02::1"
- "host.docker.internal:host-gateway" - "whiskey=ff02::2"
- "foxtrot=[ff02::3]"
- "bravo:[ff02::4]"
- "host.docker.internal=host-gateway"
- "noaddress"
`) `)
assert.NilError(t, err) assert.NilError(t, err)
expected := types.HostsList{ expected := types.HostsList{
"zulu:162.242.195.82", "zulu:162.242.195.82",
"whiskey:162.242.195.83",
"alpha:50.31.209.229", "alpha:50.31.209.229",
"zulu:ff02::1", "zulu:ff02::1",
"whiskey:ff02::2",
"foxtrot:ff02::3",
"bravo:ff02::4",
"host.docker.internal:host-gateway", "host.docker.internal:host-gateway",
} }