Skip to content

Commit 209346c

Browse files
ndeloofglours
authored andcommitted
host list can be used to declare both IPv4 and IPv6 for same hostname
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent c2ed46d commit 209346c

File tree

4 files changed

+102
-81
lines changed

4 files changed

+102
-81
lines changed

loader/full-struct_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ func services(workingDir, homeDir string) types.Services {
193193
"project_db_1:postgresql",
194194
},
195195
ExtraHosts: types.HostsList{
196-
"somehost": "162.242.195.82",
197-
"otherhost": "50.31.209.229",
196+
"somehost": []string{"162.242.195.82"},
197+
"otherhost": []string{"50.31.209.229"},
198198
},
199199
Extensions: map[string]interface{}{
200200
"x-bar": "baz",

loader/loader_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,8 +1454,8 @@ services:
14541454
assert.NilError(t, err)
14551455

14561456
expected := types.HostsList{
1457-
"alpha": "50.31.209.229",
1458-
"zulu": "162.242.195.82",
1457+
"alpha": []string{"50.31.209.229"},
1458+
"zulu": []string{"162.242.195.82"},
14591459
}
14601460

14611461
assert.Assert(t, is.Len(config.Services, 1))
@@ -1470,13 +1470,14 @@ services:
14701470
image: busybox
14711471
extra_hosts:
14721472
- "alpha:50.31.209.229"
1473+
- "zulu:127.0.0.2"
14731474
- "zulu:ff02::1"
14741475
`)
14751476
assert.NilError(t, err)
14761477

14771478
expected := types.HostsList{
1478-
"alpha": "50.31.209.229",
1479-
"zulu": "ff02::1",
1479+
"alpha": []string{"50.31.209.229"},
1480+
"zulu": []string{"127.0.0.2", "ff02::1"},
14801481
}
14811482

14821483
assert.Assert(t, is.Len(config.Services, 1))

types/hostList.go

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,33 @@ import (
2424
)
2525

2626
// HostsList is a list of colon-separated host-ip mappings
27-
type HostsList map[string]string
27+
type HostsList map[string][]string
28+
29+
// NewHostsList creates a HostsList from a list of `host=ip` strings
30+
func NewHostsList(hosts []string) (HostsList, error) {
31+
list := HostsList{}
32+
for _, s := range hosts {
33+
var found bool
34+
for _, sep := range hostListSerapators {
35+
host, ip, ok := strings.Cut(s, sep)
36+
if ok {
37+
// Mapping found with this separator, stop here.
38+
if ips, ok := list[host]; ok {
39+
list[host] = append(ips, ip)
40+
} else {
41+
list[host] = []string{ip}
42+
}
43+
found = true
44+
break
45+
}
46+
}
47+
if !found {
48+
return nil, fmt.Errorf("invalid additional host, missing IP: %s", s)
49+
}
50+
}
51+
err := list.cleanup()
52+
return list, err
53+
}
2854

2955
// AsList returns host-ip mappings as a list of strings, using the given
3056
// separator. The Docker Engine API expects ':' separators, the original format
@@ -34,7 +60,9 @@ type HostsList map[string]string
3460
func (h HostsList) AsList(sep string) []string {
3561
l := make([]string, 0, len(h))
3662
for k, v := range h {
37-
l = append(l, fmt.Sprintf("%s%s%s", k, sep, v))
63+
for _, ip := range v {
64+
l = append(l, fmt.Sprintf("%s%s%s", k, sep, ip))
65+
}
3866
}
3967
return l
4068
}
@@ -51,6 +79,8 @@ func (h HostsList) MarshalJSON() ([]byte, error) {
5179
return json.Marshal(list)
5280
}
5381

82+
var hostListSerapators = []string{"=", ":"}
83+
5484
func (h *HostsList) DecodeMapstructure(value interface{}) error {
5585
switch v := value.(type) {
5686
case map[string]interface{}:
@@ -59,25 +89,45 @@ func (h *HostsList) DecodeMapstructure(value interface{}) error {
5989
if e == nil {
6090
e = ""
6191
}
62-
list[i] = fmt.Sprint(e)
92+
list[i] = []string{fmt.Sprint(e)}
93+
}
94+
err := list.cleanup()
95+
if err != nil {
96+
return err
6397
}
6498
*h = list
99+
return nil
65100
case []interface{}:
66-
*h = decodeMapping(v, "=", ":")
101+
s := make([]string, len(v))
102+
for i, e := range v {
103+
s[i] = fmt.Sprint(e)
104+
}
105+
list, err := NewHostsList(s)
106+
if err != nil {
107+
return err
108+
}
109+
*h = list
110+
return nil
67111
default:
68112
return fmt.Errorf("unexpected value type %T for mapping", value)
69113
}
70-
for host, ip := range *h {
114+
}
115+
116+
func (h HostsList) cleanup() error {
117+
for host, ips := range h {
71118
// Check that there is a hostname and that it doesn't contain either
72119
// of the allowed separators, to generate a clearer error than the
73120
// engine would do if it splits the string differently.
74121
if host == "" || strings.ContainsAny(host, ":=") {
75122
return fmt.Errorf("bad host name '%s'", host)
76123
}
77-
// Remove brackets from IP addresses (for example "[::1]" -> "::1").
78-
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
79-
(*h)[host] = ip[1 : len(ip)-1]
124+
for i, ip := range ips {
125+
// Remove brackets from IP addresses (for example "[::1]" -> "::1").
126+
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
127+
ips[i] = ip[1 : len(ip)-1]
128+
}
80129
}
130+
h[host] = ips
81131
}
82132
return nil
83133
}

types/hostList_test.go

Lines changed: 37 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -25,107 +25,103 @@ import (
2525
is "gotest.tools/v3/assert/cmp"
2626
)
2727

28-
func TestHostsList(t *testing.T) {
28+
func TestHostsListEqual(t *testing.T) {
29+
testHostsList(t, "=")
30+
}
31+
32+
func TestHostsListComa(t *testing.T) {
33+
testHostsList(t, ":")
34+
}
35+
36+
func testHostsList(t *testing.T, sep string) {
2937
testCases := []struct {
3038
doc string
31-
input map[string]any
39+
input []string
3240
expectedError string
3341
expectedOut string
3442
}{
3543
{
3644
doc: "IPv4",
37-
input: map[string]any{"myhost": "192.168.0.1"},
45+
input: []string{"myhost" + sep + "192.168.0.1"},
3846
expectedOut: "myhost:192.168.0.1",
3947
},
4048
{
4149
doc: "Weird but permitted, IPv4 with brackets",
42-
input: map[string]any{"myhost": "[192.168.0.1]"},
50+
input: []string{"myhost" + sep + "[192.168.0.1]"},
4351
expectedOut: "myhost:192.168.0.1",
4452
},
4553
{
4654
doc: "Host and domain",
47-
input: map[string]any{"host.invalid": "10.0.2.1"},
55+
input: []string{"host.invalid" + sep + "10.0.2.1"},
4856
expectedOut: "host.invalid:10.0.2.1",
4957
},
5058
{
5159
doc: "IPv6",
52-
input: map[string]any{"anipv6host": "2003:ab34:e::1"},
60+
input: []string{"anipv6host" + sep + "2003:ab34:e::1"},
5361
expectedOut: "anipv6host:2003:ab34:e::1",
5462
},
5563
{
5664
doc: "IPv6, brackets",
57-
input: map[string]any{"anipv6host": "[2003:ab34:e::1]"},
65+
input: []string{"anipv6host" + sep + "[2003:ab34:e::1]"},
5866
expectedOut: "anipv6host:2003:ab34:e::1",
5967
},
6068
{
6169
doc: "IPv6 localhost",
62-
input: map[string]any{"ipv6local": "::1"},
70+
input: []string{"ipv6local" + sep + "::1"},
6371
expectedOut: "ipv6local:::1",
6472
},
6573
{
6674
doc: "IPv6 localhost, brackets",
67-
input: map[string]any{"ipv6local": "[::1]"},
75+
input: []string{"ipv6local" + sep + "[::1]"},
6876
expectedOut: "ipv6local:::1",
6977
},
7078
{
7179
doc: "host-gateway special case",
72-
input: map[string]any{"host.docker.internal": "host-gateway"},
80+
input: []string{"host.docker.internal" + sep + "host-gateway"},
7381
expectedOut: "host.docker.internal:host-gateway",
7482
},
7583
{
7684
doc: "multiple inputs",
77-
input: map[string]any{
78-
"myhost": "192.168.0.1",
79-
"anipv6host": "[2003:ab34:e::1]",
80-
"host.docker.internal": "host-gateway",
85+
input: []string{
86+
"myhost" + sep + "192.168.0.1",
87+
"anipv6host" + sep + "[2003:ab34:e::1]",
88+
"host.docker.internal" + sep + "host-gateway",
8189
},
8290
expectedOut: "anipv6host:2003:ab34:e::1 host.docker.internal:host-gateway myhost:192.168.0.1",
8391
},
84-
{
85-
// This won't work, but address validation is left to the engine.
86-
doc: "no ip",
87-
input: map[string]any{"myhost": nil},
88-
expectedOut: "myhost:",
89-
},
9092
{
9193
doc: "bad host, colon",
92-
input: map[string]any{":": "::1"},
94+
input: []string{"::::1"},
9395
expectedError: "bad host name",
9496
},
9597
{
9698
doc: "bad host, eq",
97-
input: map[string]any{"=": "::1"},
99+
input: []string{"=::1"},
98100
expectedError: "bad host name",
99101
},
100-
}
101-
102-
inputAsList := func(input map[string]any, sep string) []any {
103-
result := make([]any, 0, len(input))
104-
for host, ip := range input {
105-
if ip == nil {
106-
result = append(result, host+sep)
107-
} else {
108-
result = append(result, host+sep+ip.(string))
109-
}
110-
}
111-
return result
102+
{
103+
doc: "both ipv4 and ipv6",
104+
input: []string{
105+
"foo:127.0.0.2",
106+
"foo:ff02::1",
107+
},
108+
expectedOut: "foo:127.0.0.2 foo:ff02::1",
109+
},
112110
}
113111

114112
for _, tc := range testCases {
115-
// Decode the input map, check the output is as-expected.
116-
var hlFromMap HostsList
117-
t.Run(tc.doc+"_map", func(t *testing.T) {
118-
err := hlFromMap.DecodeMapstructure(tc.input)
113+
t.Run(tc.doc, func(t *testing.T) {
114+
hostlist, err := NewHostsList(tc.input)
119115
if tc.expectedError == "" {
120116
assert.NilError(t, err)
121-
actualOut := hlFromMap.AsList(":")
117+
actualOut := hostlist.AsList(":")
122118
sort.Strings(actualOut)
123119
sortedActualStr := strings.Join(actualOut, " ")
124120
assert.Check(t, is.Equal(sortedActualStr, tc.expectedOut))
125121

126122
// The YAML rendering of HostsList should be the same as the AsList() output, but
127123
// with '=' separators.
128-
yamlOut, err := hlFromMap.MarshalYAML()
124+
yamlOut, err := hostlist.MarshalYAML()
129125
assert.NilError(t, err)
130126
expYAMLOut := make([]string, len(actualOut))
131127
for i, s := range actualOut {
@@ -135,7 +131,7 @@ func TestHostsList(t *testing.T) {
135131

136132
// The JSON rendering of HostsList should also have '=' separators. Same as the
137133
// YAML output, but as a JSON list of strings.
138-
jsonOut, err := hlFromMap.MarshalJSON()
134+
jsonOut, err := hostlist.MarshalJSON()
139135
assert.NilError(t, err)
140136
expJSONStrings := make([]string, len(expYAMLOut))
141137
for i, s := range expYAMLOut {
@@ -147,31 +143,5 @@ func TestHostsList(t *testing.T) {
147143
assert.ErrorContains(t, err, tc.expectedError)
148144
}
149145
})
150-
151-
// Convert the input into a ':' separated list, check that the result is the same
152-
// as for the map-input.
153-
t.Run(tc.doc+"_colon_sep", func(t *testing.T) {
154-
var hl HostsList
155-
err := hl.DecodeMapstructure(inputAsList(tc.input, ":"))
156-
if tc.expectedError == "" {
157-
assert.NilError(t, err)
158-
assert.DeepEqual(t, hl, hlFromMap)
159-
} else {
160-
assert.ErrorContains(t, err, tc.expectedError)
161-
}
162-
})
163-
164-
// Convert the input into a ':' separated list, check that the result is the same
165-
// as for the map-input.
166-
t.Run(tc.doc+"_eq_sep", func(t *testing.T) {
167-
var hl HostsList
168-
err := hl.DecodeMapstructure(inputAsList(tc.input, "="))
169-
if tc.expectedError == "" {
170-
assert.NilError(t, err)
171-
assert.DeepEqual(t, hl, hlFromMap)
172-
} else {
173-
assert.ErrorContains(t, err, tc.expectedError)
174-
}
175-
})
176146
}
177147
}

0 commit comments

Comments
 (0)