Repository: compose-spec/compose-go
Pull request #595 (2024-03-12 07:59:56)

normalization creates implicit `default` network only when used by some service
`default` implicit network should not be introduced by normalization if not explicitly used by a service.

This let `normalizeNetworks` detect `default" network is used by some services and create this implicit network only if required


Patch:
diff --git a/loader/normalize.go b/loader/normalize.go
index 551b4be..5f7c8e8 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -26,18 +26,12 @@ import (
 
 // Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
 func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
-	dict["networks"] = normalizeNetworks(dict)
+	normalizeNetworks(dict)
 
 	if d, ok := dict["services"]; ok {
 		services := d.(map[string]any)
 		for name, s := range services {
 			service := s.(map[string]any)
-			_, hasNetworks := service["networks"]
-			_, hasNetworkMode := service["network_mode"]
-			if !hasNetworks && !hasNetworkMode {
-				// Service without explicit network attachment are implicitly exposed on default network
-				service["networks"] = map[string]any{"default": nil}
-			}
 
 			if service["pull_policy"] == types.PullPolicyIfNotPresent {
 				service["pull_policy"] = types.PullPolicyMissing
@@ -137,18 +131,51 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
 	return dict, nil
 }
 
-func normalizeNetworks(dict map[string]any) map[string]any {
+func normalizeNetworks(dict map[string]any) {
 	var networks map[string]any
 	if n, ok := dict["networks"]; ok {
 		networks = n.(map[string]any)
 	} else {
 		networks = map[string]any{}
 	}
-	if _, ok := networks["default"]; !ok {
+
+	// implicit `default` network must be introduced only if actually used by some service
+	usesDefaultNetwork := false
+
+	if s, ok := dict["services"]; ok {
+		services := s.(map[string]any)
+		for name, se := range services {
+			service := se.(map[string]any)
+			if _, ok := service["network_mode"]; ok {
+				continue
+			}
+			if n, ok := service["networks"]; !ok {
+				// If none explicitly declared, service is connected to default network
+				service["networks"] = map[string]any{"default": nil}
+				usesDefaultNetwork = true
+			} else {
+				net := n.(map[string]any)
+				if len(net) == 0 {
+					// networks section declared but empty (corner case)
+					service["networks"] = map[string]any{"default": nil}
+					usesDefaultNetwork = true
+				} else if _, ok := net["default"]; ok {
+					usesDefaultNetwork = true
+				}
+			}
+			services[name] = service
+		}
+		dict["services"] = services
+	}
+
+	if _, ok := networks["default"]; !ok && usesDefaultNetwork {
 		// If not declared explicitly, Compose model involves an implicit "default" network
 		networks["default"] = nil
 	}
-	return networks
+
+	if len(networks) > 0 {
+		dict["networks"] = networks
+	}
 }
 
 func resolve(a any, fn func(s string) (string, bool)) (any, bool) {


Test patch:
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index 09f6615..5e41d50 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -88,9 +88,6 @@ volumes:
 
 	expected := `
 name: myProject
-networks: 
-  default:
-    name: myProject_default
 volumes:  
   myExternalVol: 
     name: myExternalVol
@@ -275,3 +272,67 @@ networks:
 	assert.NilError(t, err)
 	assert.DeepEqual(t, expect, model)
 }
+
+func TestNormalizeDefaultNetwork(t *testing.T) {
+	project := `
+name: myProject
+services:  
+  test:
+    image: test
+`
+
+	expected := `
+name: myProject
+networks:
+  default:
+    name: myProject_default
+services:  
+  test: 
+    image: test
+    networks:
+      default: null
+`
+	var model map[string]any
+	err := yaml.Unmarshal([]byte(project), &model)
+	assert.NilError(t, err)
+	model, err = Normalize(model, nil)
+	assert.NilError(t, err)
+
+	var expect map[string]any
+	err = yaml.Unmarshal([]byte(expected), &expect)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expect, model)
+}
+
+func TestNormalizeCustomNetwork(t *testing.T) {
+	project := `
+name: myProject
+services:  
+  test: 
+    networks:
+      my_network: null
+networks:
+  my_network: null
+`
+
+	expected := `
+name: myProject
+networks:
+  my_network:
+    name: myProject_my_network
+services:  
+  test: 
+    networks:
+      my_network: null
+`
+	var model map[string]any
+	err := yaml.Unmarshal([]byte(project), &model)
+	assert.NilError(t, err)
+	model, err = Normalize(model, nil)
+	assert.NilError(t, err)
+
+	var expect map[string]any
+	err = yaml.Unmarshal([]byte(expected), &expect)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expect, model)
+}