Repository: compose-spec/compose-go
Pull request #678 (2024-09-02 15:33:19)

Support multiple values for extra_hosts
This PR adds support for multiple-value set by `extra_hosts`, either with long syntax _or_ single line syntax, using a coma separator between IPs in value. Unicity doesn't apply to extra_hosts as this `docker run` flag actually supports multiple values per key being set, so it was wrong for compose to consider an override.


closes https://github.com/docker/compose/issues/12088


Patch:
diff --git a/override/uncity.go b/override/uncity.go
index 9d1b895..3b0c63d 100644
--- a/override/uncity.go
+++ b/override/uncity.go
@@ -36,7 +36,6 @@ func init() {
 	unique["services.*.annotations"] = keyValueIndexer
 	unique["services.*.build.args"] = keyValueIndexer
 	unique["services.*.build.additional_contexts"] = keyValueIndexer
-	unique["services.*.build.extra_hosts"] = keyValueIndexer
 	unique["services.*.build.platform"] = keyValueIndexer
 	unique["services.*.build.tags"] = keyValueIndexer
 	unique["services.*.build.labels"] = keyValueIndexer
@@ -51,7 +50,6 @@ func init() {
 	unique["services.*.environment"] = keyValueIndexer
 	unique["services.*.env_file"] = envFileIndexer
 	unique["services.*.expose"] = exposeIndexer
-	unique["services.*.extra_hosts"] = keyValueIndexer
 	unique["services.*.labels"] = keyValueIndexer
 	unique["services.*.links"] = keyValueIndexer
 	unique["services.*.networks.*.aliases"] = keyValueIndexer
@@ -109,16 +107,16 @@ func enforceUnicity(value any, p tree.Path) (any, error) {
 	return value, nil
 }
 
-func keyValueIndexer(y any, p tree.Path) (string, error) {
-	switch value := y.(type) {
+func keyValueIndexer(v any, p tree.Path) (string, error) {
+	switch value := v.(type) {
 	case string:
 		key, _, found := strings.Cut(value, "=")
-		if !found {
-			return value, nil
+		if found {
+			return key, nil
 		}
-		return key, nil
+		return value, nil
 	default:
-		return "", fmt.Errorf("%s: unexpected type %T", p, y)
+		return "", fmt.Errorf("%s: unexpected type %T", p, v)
 	}
 }
 
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index cb4f89e..df3cf32 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -115,7 +115,7 @@
                 "pull": {"type": ["boolean", "string"]},
                 "target": {"type": "string"},
                 "shm_size": {"type": ["integer", "string"]},
-                "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+                "extra_hosts": {"$ref": "#/definitions/extra_hosts"},
                 "isolation": {"type": "string"},
                 "privileged": {"type": ["boolean", "string"]},
                 "secrets": {"$ref": "#/definitions/service_config_or_secret"},
@@ -267,7 +267,7 @@
           ]
         },
         "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
-        "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+        "extra_hosts": {"$ref": "#/definitions/extra_hosts"},
         "group_add": {
           "type": "array",
           "items": {
@@ -872,6 +872,21 @@
       ]
     },
 
+    "extra_hosts": {
+      "oneOf": [
+        {
+          "type": "object",
+          "patternProperties": {
+            ".+": {
+              "type": ["string", "array"]
+            }
+          },
+          "additionalProperties": false
+        },
+        {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+      ]
+    },
+
     "blkio_limit": {
       "type": "object",
       "properties": {
diff --git a/types/hostList.go b/types/hostList.go
index e224b81..9bc0fbc 100644
--- a/types/hostList.go
+++ b/types/hostList.go
@@ -36,9 +36,9 @@ func NewHostsList(hosts []string) (HostsList, error) {
 			if ok {
 				// Mapping found with this separator, stop here.
 				if ips, ok := list[host]; ok {
-					list[host] = append(ips, ip)
+					list[host] = append(ips, strings.Split(ip, ",")...)
 				} else {
-					list[host] = []string{ip}
+					list[host] = strings.Split(ip, ",")
 				}
 				found = true
 				break
@@ -89,7 +89,18 @@ func (h *HostsList) DecodeMapstructure(value interface{}) error {
 			if e == nil {
 				e = ""
 			}
-			list[i] = []string{fmt.Sprint(e)}
+			switch t := e.(type) {
+			case string:
+				list[i] = []string{t}
+			case []any:
+				hosts := make([]string, len(t))
+				for j, h := range t {
+					hosts[j] = fmt.Sprint(h)
+				}
+				list[i] = hosts
+			default:
+				return fmt.Errorf("unexpected value type %T for extra_hosts entry", value)
+			}
 		}
 		err := list.cleanup()
 		if err != nil {
@@ -109,7 +120,7 @@ func (h *HostsList) DecodeMapstructure(value interface{}) error {
 		*h = list
 		return nil
 	default:
-		return fmt.Errorf("unexpected value type %T for mapping", value)
+		return fmt.Errorf("unexpected value type %T for extra_hosts", value)
 	}
 }
 


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 01ae3eb..cba477f 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3377,3 +3377,35 @@ services:
 		},
 	})
 }
+
+func TestLoadExtraHostsRepeated(t *testing.T) {
+	p, err := loadYAML(`
+name: load-extra-hosts
+services:
+  test:
+    extra_hosts:
+      - "myhost=0.0.0.1,0.0.0.2"
+`)
+	assert.NilError(t, err)
+	hosts := p.Services["test"].ExtraHosts
+	assert.DeepEqual(t, hosts, types.HostsList{
+		"myhost": []string{"0.0.0.1", "0.0.0.2"},
+	})
+}
+
+func TestLoadExtraHostsLongSyntax(t *testing.T) {
+	p, err := loadYAML(`
+name: load-extra-hosts
+services:
+  test:
+    extra_hosts:
+      myhost:
+        - "0.0.0.1"
+        - "0.0.0.2"
+`)
+	assert.NilError(t, err)
+	hosts := p.Services["test"].ExtraHosts
+	assert.DeepEqual(t, hosts, types.HostsList{
+		"myhost": []string{"0.0.0.1", "0.0.0.2"},
+	})
+}
diff --git a/override/merge_build_test.go b/override/merge_build_test.go
index 8f0c057..c141166 100644
--- a/override/merge_build_test.go
+++ b/override/merge_build_test.go
@@ -82,6 +82,7 @@ services:
         - "registry/username/myrepos:my-other-tag"
       extra_hosts:
         - "somehost=162.242.195.82"
+        - "otherhost=50.31.209.229"
         - "otherhost=50.31.209.230"
         - "myhostv6=::1"
 `)
diff --git a/types/hostList_test.go b/types/hostList_test.go
index b32e350..2b0264f 100644
--- a/types/hostList_test.go
+++ b/types/hostList_test.go
@@ -107,6 +107,13 @@ func testHostsList(t *testing.T, sep string) {
 			},
 			expectedOut: "foo:127.0.0.2 foo:ff02::1",
 		},
+		{
+			doc: "list of values",
+			input: []string{
+				"foo=127.0.0.2,127.0.0.3",
+			},
+			expectedOut: "foo:127.0.0.2 foo:127.0.0.3",
+		},
 	}
 
 	for _, tc := range testCases {