Repository: compose-spec/compose-go
Pull request #94 (2021-01-13 14:29:42)

Fix network naming in normalization
If not specified in yaml, network names are normalized to `project_networkKey` , this is the final network name used to create network by docker-compose or compose-cli implementation. 
Previously it was normalized to just `networkKey` and later on we have no way to know if this was specified by user or not, when adding the `project_` prefix.

Same for volumes, config & secrets for consistency. 
This impact downstream projects : there is no additional name manipulation needed to add a prefix




Patch:
diff --git a/loader/loader.go b/loader/loader.go
index a67c2a9..4254749 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -229,60 +229,35 @@ func loadSections(filename string, config map[string]interface{}, configDetails
 		Filename: filename,
 	}
 
-	var loaders = []struct {
-		key string
-		fnc func(config map[string]interface{}) error
-	}{
-		{
-			key: "services",
-			fnc: func(config map[string]interface{}) error {
-				cfg.Services, err = LoadServices(filename, config, configDetails.WorkingDir, configDetails.LookupEnv, opts)
-				return err
-			},
-		},
-		{
-			key: "networks",
-			fnc: func(config map[string]interface{}) error {
-				cfg.Networks, err = LoadNetworks(config, configDetails.Version)
-				return err
-			},
-		},
-		{
-			key: "volumes",
-			fnc: func(config map[string]interface{}) error {
-				cfg.Volumes, err = LoadVolumes(config)
-				return err
-			},
-		},
-		{
-			key: "secrets",
-			fnc: func(config map[string]interface{}) error {
-				cfg.Secrets, err = LoadSecrets(config, configDetails)
-				return err
-			},
-		},
-		{
-			key: "configs",
-			fnc: func(config map[string]interface{}) error {
-				cfg.Configs, err = LoadConfigObjs(config, configDetails)
-				return err
-			},
-		},
-		{
-			key: "extensions",
-			fnc: func(config map[string]interface{}) error {
-				if len(config) > 0 {
-					cfg.Extensions = config
-				}
-				return err
-			},
-		},
+	cfg.Services, err = LoadServices(filename, getSection(config, "services"), configDetails.WorkingDir, configDetails.LookupEnv, opts)
+	if err != nil {
+		return nil, err
 	}
-	for _, loader := range loaders {
-		if err := loader.fnc(getSection(config, loader.key)); err != nil {
-			return nil, err
-		}
+
+	cfg.Networks, err = LoadNetworks(getSection(config, "networks"), configDetails.Version)
+	if err != nil {
+		return nil, err
+	}
+	cfg.Volumes, err = LoadVolumes(getSection(config, "volumes"))
+	if err != nil {
+		return nil, err
+	}
+	cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails)
+	if err != nil {
+		return nil, err
 	}
+	cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails)
+	if err != nil {
+		return nil, err
+	}
+	extensions := getSection(config, "extensions")
+	if len(extensions) > 0 {
+		cfg.Extensions = extensions
+	}
+	if err != nil {
+		return nil, err
+	}
+
 	return &cfg, nil
 }
 
@@ -294,31 +269,6 @@ func getSection(config map[string]interface{}, key string) map[string]interface{
 	return section.(map[string]interface{})
 }
 
-func sortedKeys(set map[string]bool) []string {
-	var keys []string
-	for key := range set {
-		keys = append(keys, key)
-	}
-	sort.Strings(keys)
-	return keys
-}
-
-func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
-	output := map[string]string{}
-
-	for _, service := range services {
-		if serviceDict, ok := service.(map[string]interface{}); ok {
-			for property, description := range propertyMap {
-				if _, isSet := serviceDict[property]; isSet {
-					output[property] = description
-				}
-			}
-		}
-	}
-
-	return output
-}
-
 // ForbiddenPropertiesError is returned when there are properties in the Compose
 // file that are forbidden.
 type ForbiddenPropertiesError struct {
@@ -329,16 +279,6 @@ func (e *ForbiddenPropertiesError) Error() string {
 	return "Configuration contains forbidden properties"
 }
 
-func getServices(configDict map[string]interface{}) map[string]interface{} {
-	if services, ok := configDict["services"]; ok {
-		if servicesDict, ok := services.(map[string]interface{}); ok {
-			return servicesDict
-		}
-	}
-
-	return map[string]interface{}{}
-}
-
 // Transform converts the source into the target struct with compose types transformer
 // and the specified transformers if any.
 func Transform(source interface{}, target interface{}, additionalTransformers ...Transformer) error {
diff --git a/loader/normalize.go b/loader/normalize.go
index eef0e74..c38038f 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -17,6 +17,8 @@
 package loader
 
 import (
+	"fmt"
+
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
@@ -68,28 +70,28 @@ func normalize(project *types.Project) error {
 func setNameFromKey(project *types.Project) {
 	for i, n := range project.Networks {
 		if n.Name == "" {
-			n.Name = i
+			n.Name = fmt.Sprintf("%s_%s", project.Name, i)
 			project.Networks[i] = n
 		}
 	}
 
 	for i, v := range project.Volumes {
 		if v.Name == "" {
-			v.Name = i
+			v.Name = fmt.Sprintf("%s_%s", project.Name, i)
 			project.Volumes[i] = v
 		}
 	}
 
 	for i, c := range project.Configs {
 		if c.Name == "" {
-			c.Name = i
+			c.Name = fmt.Sprintf("%s_%s", project.Name, i)
 			project.Configs[i] = c
 		}
 	}
 
 	for i, s := range project.Secrets {
 		if s.Name == "" {
-			s.Name = i
+			s.Name = fmt.Sprintf("%s_%s", project.Name, i)
 			project.Secrets[i] = s
 		}
 	}


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 48bc100..49fa36e 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -282,7 +282,7 @@ func TestLoadExtends(t *testing.T) {
 services:
   foo:
     image: busybox
-    extends: 
+    extends:
       service: bar
   bar:
     image: alpine
@@ -579,7 +579,7 @@ networks:
     external: $thebool
     internal: $thebool
     attachable: $thebool
-
+  back:
 `))
 	assert.NilError(t, err)
 	env := map[string]string{
@@ -669,6 +669,7 @@ networks:
 			"data": {External: types.External{External: true}, Name: "data"},
 		},
 		Networks: map[string]types.NetworkConfig{
+			"back": {},
 			"front": {
 				External:   types.External{External: true},
 				Name:       "front",
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
new file mode 100644
index 0000000..817bcf6
--- /dev/null
+++ b/loader/normalize_test.go
@@ -0,0 +1,92 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package loader
+
+import (
+	"testing"
+
+	"github.com/compose-spec/compose-go/types"
+	"gotest.tools/v3/assert"
+)
+
+func TestNormalizeNetworkNames(t *testing.T) {
+	project := types.Project{
+		Name: "myProject",
+		Networks: types.Networks{
+			"myExternalnet": {
+				Name:     "myExternalnet", // this is automaticaly setup by loader for externa networks before reaching normalization
+				External: types.External{External: true},
+			},
+			"mynet": {},
+			"myNamedNet": {
+				Name: "CustomName",
+			},
+		},
+	}
+
+	expected := types.Project{
+		Name: "myProject",
+		Networks: types.Networks{
+			"myExternalnet": {
+				Name:     "myExternalnet",
+				External: types.External{External: true},
+			},
+			"mynet": {Name: "myProject_mynet"},
+			"myNamedNet": {
+				Name: "CustomName",
+			},
+		},
+	}
+	err := normalize(&project)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expected, project)
+}
+
+func TestNormalizeVolumes(t *testing.T) {
+	project := types.Project{
+		Name:     "myProject",
+		Networks: types.Networks{},
+		Volumes: types.Volumes{
+			"myExternalVol": {
+				Name:     "myExternalVol", // this is automaticaly setup by loader for externa networks before reaching normalization
+				External: types.External{External: true},
+			},
+			"myvol": {},
+			"myNamedVol": {
+				Name: "CustomName",
+			},
+		},
+	}
+
+	expected := types.Project{
+		Name:     "myProject",
+		Networks: types.Networks{"default": {Name: "myProject_default"}},
+		Volumes: types.Volumes{
+			"myExternalVol": {
+				Name:     "myExternalVol",
+				External: types.External{External: true},
+			},
+			"myvol": {Name: "myProject_myvol"},
+			"myNamedVol": {
+				Name: "CustomName",
+			},
+		},
+	}
+	err := normalize(&project)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expected, project)
+}