Repository: compose-spec/compose-go
Pull request #208 (2021-11-24 07:47:59)

introduce CustomLabels
introduce ServiceConfig#CustomLabels to be used by compose implementation to store metadata that are **not** to be serialized as part of the compose canonical configuration.

Compose V2 can then use such labels to set `com.docker.compose.*` labels but get those excluded from the marshaled configuration or service hash. see https://github.com/docker/compose/pull/8960

see https://github.com/docker/compose/issues/8725


Patch:
diff --git a/compatibility/services.go b/compatibility/services.go
index fc68dc2..39bca64 100644
--- a/compatibility/services.go
+++ b/compatibility/services.go
@@ -169,10 +169,10 @@ func (c *AllowList) CheckConfigs(service *types.ServiceConfig) {
 			c.Unsupported("services.configs")
 			return
 		}
-		for i, conf := range service.Configs {
-			ref := types.FileReferenceConfig(conf)
+		for i, s := range service.Secrets {
+			ref := types.FileReferenceConfig(s)
 			c.CheckFileReference("configs", &ref)
-			service.Configs[i] = conf
+			service.Secrets[i] = s
 		}
 	}
 }
diff --git a/go.mod b/go.mod
index d120a1c..91bc13e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/compose-spec/compose-go
 go 1.16
 
 require (
-	github.com/compose-spec/godotenv v1.1.1
+	github.com/compose-spec/godotenv v1.1.0
 	github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e
 	github.com/docker/go-connections v0.4.0
 	github.com/docker/go-units v0.4.0
diff --git a/go.sum b/go.sum
index 72f83cc..4b09240 100644
--- a/go.sum
+++ b/go.sum
@@ -14,8 +14,8 @@ github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
-github.com/compose-spec/godotenv v1.1.1 h1:lp+WpAInnw06YN9sV/XLUOV/9z4C+6wjJdWlrdVac7o=
-github.com/compose-spec/godotenv v1.1.1/go.mod h1:zF/3BOa18Z24tts5qnO/E9YURQanJTBUf7nlcCTNsyc=
+github.com/compose-spec/godotenv v1.1.0 h1:wzShe5P6L/Aw3wsV357eWlZdMcPaOe2V2+3+qGwMEL4=
+github.com/compose-spec/godotenv v1.1.0/go.mod h1:zF/3BOa18Z24tts5qnO/E9YURQanJTBUf7nlcCTNsyc=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/loader/full-example.yml b/loader/full-example.yml
index 529d28b..9463445 100644
--- a/loader/full-example.yml
+++ b/loader/full-example.yml
@@ -268,7 +268,7 @@ services:
       - .:/code
       - ./static:/var/www/html
       # User-relative path
-      - ~/configs:/etc/configs:ro
+      - ~/configs:/etc/configs/:ro
       # Named volume
       - datavolume:/var/lib/mysql
       - type: bind
diff --git a/loader/loader.go b/loader/loader.go
index b55b005..159e9c7 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -145,7 +145,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 		op(opts)
 	}
 
-	var configs []*types.Config
+	configs := []*types.Config{}
 	for i, file := range configDetails.ConfigFiles {
 		configDict := file.Config
 		if configDict == nil {
@@ -222,14 +222,14 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 }
 
 func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) {
-	yml, err := ParseYAML(b)
+	yaml, err := ParseYAML(b)
 	if err != nil {
 		return nil, err
 	}
 	if !opts.SkipInterpolation {
-		return interp.Interpolate(yml, *opts.Interpolate)
+		return interp.Interpolate(yaml, *opts.Interpolate)
 	}
-	return yml, err
+	return yaml, err
 }
 
 func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} {
@@ -342,8 +342,8 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
 		reflect.TypeOf(types.UlimitsConfig{}):                    transformUlimits,
 		reflect.TypeOf(types.UnitBytes(0)):                       transformSize,
 		reflect.TypeOf([]types.ServicePortConfig{}):              transformServicePort,
-		reflect.TypeOf(types.ServiceSecretConfig{}):              transformFileReferenceConfig,
-		reflect.TypeOf(types.ServiceConfigObjConfig{}):           transformFileReferenceConfig,
+		reflect.TypeOf(types.ServiceSecretConfig{}):              transformStringSourceMap,
+		reflect.TypeOf(types.ServiceConfigObjConfig{}):           transformStringSourceMap,
 		reflect.TypeOf(types.StringOrNumberList{}):               transformStringOrNumberList,
 		reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap,
 		reflect.TypeOf(types.Mapping{}):                          transformMappingOrListFunc("=", false),
@@ -372,7 +372,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
 	}
 }
 
-// keys need to be converted to strings for jsonschema
+// keys needs to be converted to strings for jsonschema
 func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
 	if mapping, ok := value.(map[interface{}]interface{}); ok {
 		dict := make(map[string]interface{})
@@ -396,7 +396,7 @@ func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interfac
 		return dict, nil
 	}
 	if list, ok := value.([]interface{}); ok {
-		var convertedList []interface{}
+		convertedList := []interface{}{}
 		for index, entry := range list {
 			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
 			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
@@ -532,7 +532,7 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
 	}
 
 	for i, volume := range serviceConfig.Volumes {
-		if volume.Type != types.VolumeTypeBind {
+		if volume.Type != "bind" {
 			continue
 		}
 
@@ -552,8 +552,8 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
 	environment := types.MappingWithEquals{}
 
 	if len(serviceConfig.EnvFile) > 0 {
-		for _, envFile := range serviceConfig.EnvFile {
-			filePath := absPath(workingDir, envFile)
+		for _, file := range serviceConfig.EnvFile {
+			filePath := absPath(workingDir, file)
 			file, err := os.Open(filePath)
 			if err != nil {
 				return err
@@ -797,7 +797,7 @@ var transformServicePort TransformerFunc = func(data interface{}) (interface{},
 		// We process the list instead of individual items here.
 		// The reason is that one entry might be mapped to multiple ServicePortConfig.
 		// Therefore we take an input of a list and return an output of a list.
-		var ports []interface{}
+		ports := []interface{}{}
 		for _, entry := range entries {
 			switch value := entry.(type) {
 			case int:
@@ -852,27 +852,17 @@ var transformServiceDeviceRequest TransformerFunc = func(data interface{}) (inte
 	}
 }
 
-var transformFileReferenceConfig TransformerFunc = func(data interface{}) (interface{}, error) {
+var transformStringSourceMap TransformerFunc = func(data interface{}) (interface{}, error) {
 	switch value := data.(type) {
 	case string:
 		return map[string]interface{}{"source": value}, nil
 	case map[string]interface{}:
-		if target, ok := value["target"]; ok {
-			value["target"] = cleanTarget(target.(string))
-		}
-		return groupXFieldsIntoExtensions(value), nil
+		return groupXFieldsIntoExtensions(data.(map[string]interface{})), nil
 	default:
 		return data, errors.Errorf("invalid type %T for secret", value)
 	}
 }
 
-func cleanTarget(target string) string {
-	if target == "" {
-		return ""
-	}
-	return path.Clean(target)
-}
-
 var transformBuildConfig TransformerFunc = func(data interface{}) (interface{}, error) {
 	switch value := data.(type) {
 	case string:
@@ -916,15 +906,9 @@ var transformExtendsConfig TransformerFunc = func(data interface{}) (interface{}
 var transformServiceVolumeConfig TransformerFunc = func(data interface{}) (interface{}, error) {
 	switch value := data.(type) {
 	case string:
-		volume, err := ParseVolume(value)
-		volume.Target = cleanTarget(volume.Target)
-		return volume, err
+		return ParseVolume(value)
 	case map[string]interface{}:
-		data := groupXFieldsIntoExtensions(data.(map[string]interface{}))
-		if target, ok := data["target"]; ok {
-			data["target"] = cleanTarget(target.(string))
-		}
-		return data, nil
+		return groupXFieldsIntoExtensions(data.(map[string]interface{})), nil
 	default:
 		return data, errors.Errorf("invalid type %T for service volume", value)
 	}
@@ -987,7 +971,7 @@ func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool
 	switch value := mappingOrList.(type) {
 	case map[string]interface{}:
 		return toMapStringString(value, allowNil)
-	case []interface{}:
+	case ([]interface{}):
 		result := make(map[string]interface{})
 		for _, value := range value {
 			parts := strings.SplitN(value.(string), sep, 2)
@@ -1070,7 +1054,7 @@ func toString(value interface{}, allowNil bool) interface{} {
 }
 
 func toStringList(value map[string]interface{}, separator string, allowNil bool) []string {
-	var output []string
+	output := []string{}
 	for key, value := range value {
 		if value == nil && !allowNil {
 			continue
diff --git a/loader/merge.go b/loader/merge.go
index b01c868..0c7a6dd 100644
--- a/loader/merge.go
+++ b/loader/merge.go
@@ -114,11 +114,6 @@ func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConf
 	if overrideService.Entrypoint != nil {
 		baseService.Entrypoint = overrideService.Entrypoint
 	}
-	if baseService.Environment != nil {
-		baseService.Environment.OverrideBy(overrideService.Environment)
-	} else {
-		baseService.Environment = overrideService.Environment
-	}
 	return baseService, nil
 }
 
@@ -184,7 +179,7 @@ func toServiceVolumeConfigsMap(s interface{}) (map[interface{}]interface{}, erro
 }
 
 func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
-	var s []types.ServiceSecretConfig
+	s := []types.ServiceSecretConfig{}
 	for _, v := range m {
 		s = append(s, v.(types.ServiceSecretConfig))
 	}
@@ -194,7 +189,7 @@ func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{
 }
 
 func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
-	var s []types.ServiceConfigObjConfig
+	s := []types.ServiceConfigObjConfig{}
 	for _, v := range m {
 		s = append(s, v.(types.ServiceConfigObjConfig))
 	}
@@ -204,7 +199,7 @@ func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[interface{}]interf
 }
 
 func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
-	var s []types.ServicePortConfig
+	s := []types.ServicePortConfig{}
 	for _, v := range m {
 		s = append(s, v.(types.ServicePortConfig))
 	}
@@ -225,7 +220,7 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{})
 }
 
 func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
-	var s []types.ServiceVolumeConfig
+	s := []types.ServiceVolumeConfig{}
 	for _, v := range m {
 		s = append(s, v.(types.ServiceVolumeConfig))
 	}
@@ -234,7 +229,7 @@ func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{
 	return nil
 }
 
-type toMapFn func(s interface{}) (map[interface{}]interface{}, error)
+type tomapFn func(s interface{}) (map[interface{}]interface{}, error)
 type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error
 
 func safelyMerge(mergeFn func(dst, src reflect.Value) error) func(dst, src reflect.Value) error {
@@ -250,13 +245,13 @@ func safelyMerge(mergeFn func(dst, src reflect.Value) error) func(dst, src refle
 	}
 }
 
-func mergeSlice(toMap toMapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error {
+func mergeSlice(tomap tomapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error {
 	return func(dst, src reflect.Value) error {
-		dstMap, err := sliceToMap(toMap, dst)
+		dstMap, err := sliceToMap(tomap, dst)
 		if err != nil {
 			return err
 		}
-		srcMap, err := sliceToMap(toMap, src)
+		srcMap, err := sliceToMap(tomap, src)
 		if err != nil {
 			return err
 		}
@@ -267,12 +262,12 @@ func mergeSlice(toMap toMapFn, writeValue writeValueFromMapFn) func(dst, src ref
 	}
 }
 
-func sliceToMap(toMap toMapFn, v reflect.Value) (map[interface{}]interface{}, error) {
+func sliceToMap(tomap tomapFn, v reflect.Value) (map[interface{}]interface{}, error) {
 	// check if valid
 	if !v.IsValid() {
 		return nil, errors.Errorf("invalid value : %+v", v)
 	}
-	return toMap(v.Interface())
+	return tomap(v.Interface())
 }
 
 func mergeLoggingConfig(dst, src reflect.Value) error {
diff --git a/loader/volume.go b/loader/volume.go
index dd0f612..173b76b 100644
--- a/loader/volume.go
+++ b/loader/volume.go
@@ -36,11 +36,11 @@ func ParseVolume(spec string) (types.ServiceVolumeConfig, error) {
 		return volume, errors.New("invalid empty volume spec")
 	case 1, 2:
 		volume.Target = spec
-		volume.Type = types.VolumeTypeVolume
+		volume.Type = string(types.VolumeTypeVolume)
 		return volume, nil
 	}
 
-	var buffer []rune
+	buffer := []rune{}
 	for _, char := range spec + string(endOfSpec) {
 		switch {
 		case isWindowsDrive(buffer, char):
@@ -50,7 +50,7 @@ func ParseVolume(spec string) (types.ServiceVolumeConfig, error) {
 				populateType(&volume)
 				return volume, errors.Wrapf(err, "invalid spec: %s", spec)
 			}
-			buffer = nil
+			buffer = []rune{}
 		default:
 			buffer = append(buffer, char)
 		}
diff --git a/types/project.go b/types/project.go
index 6de8e61..155fad0 100644
--- a/types/project.go
+++ b/types/project.go
@@ -46,7 +46,7 @@ type Project struct {
 
 // ServiceNames return names for all services in this Compose config
 func (p Project) ServiceNames() []string {
-	var names []string
+	names := []string{}
 	for _, s := range p.Services {
 		names = append(names, s.Name)
 	}
@@ -56,7 +56,7 @@ func (p Project) ServiceNames() []string {
 
 // VolumeNames return names for all volumes in this Compose config
 func (p Project) VolumeNames() []string {
-	var names []string
+	names := []string{}
 	for k := range p.Volumes {
 		names = append(names, k)
 	}
@@ -66,7 +66,7 @@ func (p Project) VolumeNames() []string {
 
 // NetworkNames return names for all volumes in this Compose config
 func (p Project) NetworkNames() []string {
-	var names []string
+	names := []string{}
 	for k := range p.Networks {
 		names = append(names, k)
 	}
@@ -76,7 +76,7 @@ func (p Project) NetworkNames() []string {
 
 // SecretNames return names for all secrets in this Compose config
 func (p Project) SecretNames() []string {
-	var names []string
+	names := []string{}
 	for k := range p.Secrets {
 		names = append(names, k)
 	}
@@ -86,7 +86,7 @@ func (p Project) SecretNames() []string {
 
 // ConfigNames return names for all configs in this Compose config
 func (p Project) ConfigNames() []string {
-	var names []string
+	names := []string{}
 	for k := range p.Configs {
 		names = append(names, k)
 	}
@@ -179,12 +179,12 @@ func (p *Project) RelativePath(path string) string {
 }
 
 // HasProfile return true if service has no profile declared or has at least one profile matching
-func (s ServiceConfig) HasProfile(profiles []string) bool {
-	if len(s.Profiles) == 0 {
+func (service ServiceConfig) HasProfile(profiles []string) bool {
+	if len(service.Profiles) == 0 {
 		return true
 	}
 	for _, p := range profiles {
-		for _, sp := range s.Profiles {
+		for _, sp := range service.Profiles {
 			if sp == p {
 				return true
 			}
@@ -327,6 +327,7 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (digest.Dig
 				if err != nil {
 					return err
 				}
+
 				named, err = reference.WithDigest(named, digest)
 				if err != nil {
 					return err
diff --git a/types/types.go b/types/types.go
index c19d0eb..9cc1efa 100644
--- a/types/types.go
+++ b/types/types.go
@@ -33,7 +33,7 @@ func (d Duration) String() string {
 	return time.Duration(d).String()
 }
 
-// ConvertDurationPtr converts a type defined Duration pointer to a time.Duration pointer with the same value.
+// ConvertDurationPtr converts a typedefined Duration pointer to a time.Duration pointer with the same value.
 func ConvertDurationPtr(d *Duration) *time.Duration {
 	if d == nil {
 		return nil
@@ -129,6 +129,7 @@ type ServiceConfig struct {
 	Ipc             string                           `yaml:",omitempty" json:"ipc,omitempty"`
 	Isolation       string                           `mapstructure:"isolation" yaml:"isolation,omitempty" json:"isolation,omitempty"`
 	Labels          Labels                           `yaml:",omitempty" json:"labels,omitempty"`
+	CustomLabels    Labels                           `yaml:"-" json:"-"`
 	Links           []string                         `yaml:",omitempty" json:"links,omitempty"`
 	Logging         *LoggingConfig                   `yaml:",omitempty" json:"logging,omitempty"`
 	LogDriver       string                           `mapstructure:"log_driver" yaml:"log_driver,omitempty" json:"log_driver,omitempty"`
@@ -208,7 +209,7 @@ const (
 	PullPolicyNever = "never"
 	//PullPolicyIfNotPresent pull missing images
 	PullPolicyIfNotPresent = "if_not_present"
-	//PullPolicyMissing pull missing images
+	//PullPolicyIfNotPresent pull missing images
 	PullPolicyMissing = "missing"
 	//PullPolicyBuild force building images
 	PullPolicyBuild = "build"
@@ -611,7 +612,7 @@ func ParsePortConfig(value string) ([]ServicePortConfig, error) {
 }
 
 func convertPortToPortConfig(port nat.Port, portBindings map[nat.Port][]nat.PortBinding) ([]ServicePortConfig, error) {
-	var portConfigs []ServicePortConfig
+	portConfigs := []ServicePortConfig{}
 	for _, binding := range portBindings[port] {
 		startHostPort, endHostPort, err := nat.ParsePortRange(binding.HostPort)
 
@@ -647,13 +648,13 @@ type ServiceVolumeConfig struct {
 }
 
 const (
-	// VolumeTypeBind is the type for mounting host dir
+	// TypeBind is the type for mounting host dir
 	VolumeTypeBind = "bind"
-	// VolumeTypeVolume is the type for remote storage volumes
+	// TypeVolume is the type for remote storage volumes
 	VolumeTypeVolume = "volume"
-	// VolumeTypeTmpfs is the type for mounting tmpfs
+	// TypeTmpfs is the type for mounting tmpfs
 	VolumeTypeTmpfs = "tmpfs"
-	// VolumeTypeNamedPipe is the type for mounting Windows named pipes
+	// TypeNamedPipe is the type for mounting Windows named pipes
 	VolumeTypeNamedPipe = "npipe"
 )
 


Test patch:
diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go
index 3ffcc76..a81e047 100644
--- a/loader/full-struct_test.go
+++ b/loader/full-struct_test.go
@@ -399,7 +399,7 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
 				{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind", Bind: &types.ServiceVolumeBind{CreateHostPath: true}},
 				{Source: workingDir, Target: "/code", Type: "bind", Bind: &types.ServiceVolumeBind{CreateHostPath: true}},
 				{Source: filepath.Join(workingDir, "static"), Target: "/var/www/html", Type: "bind", Bind: &types.ServiceVolumeBind{CreateHostPath: true}},
-				{Source: filepath.Join(homeDir, "/configs"), Target: "/etc/configs", Type: "bind", ReadOnly: true, Bind: &types.ServiceVolumeBind{CreateHostPath: true}},
+				{Source: filepath.Join(homeDir, "/configs"), Target: "/etc/configs/", Type: "bind", ReadOnly: true, Bind: &types.ServiceVolumeBind{CreateHostPath: true}},
 				{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume", Volume: &types.ServiceVolumeVolume{}},
 				{Source: filepath.Join(workingDir, "opt"), Target: "/opt", Consistency: "cached", Type: "bind"},
 				{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{
@@ -851,7 +851,7 @@ func fullExampleYAML(workingDir, homeDir string) string {
         create_host_path: true
     - type: bind
       source: %s
-      target: /etc/configs
+      target: /etc/configs/
       read_only: true
       bind:
         create_host_path: true
@@ -1481,7 +1481,7 @@ func fullExampleJSON(workingDir, homeDir string) string {
         {
           "type": "bind",
           "source": "%s",
-          "target": "/etc/configs",
+          "target": "/etc/configs/",
           "read_only": true,
           "bind": {
             "create_host_path": true
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 8abf0c9..ead2e17 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -1812,7 +1812,7 @@ services:
 	assert.NilError(t, err)
 }
 
-func TestLoadServiceWithEnvFile(t *testing.T) {
+func TestLoadService(t *testing.T) {
 	file, err := os.CreateTemp("", "test-compose-go")
 	assert.NilError(t, err)
 	defer os.Remove(file.Name())
@@ -1830,33 +1830,3 @@ func TestLoadServiceWithEnvFile(t *testing.T) {
 	assert.NilError(t, err)
 	assert.Equal(t, "YES", *s.Environment["HALLO"])
 }
-
-func TestLoadServiceWithVolumes(t *testing.T) {
-	m := map[string]interface{}{
-		"volumes": []interface{}{
-			"source:/path 1/",
-			map[string]interface{}{
-				"target": "/path 2/",
-			},
-		},
-		"configs": []interface{}{
-			map[string]interface{}{
-				"target": "/path 3/",
-			},
-		},
-		"secrets": []interface{}{
-			map[string]interface{}{
-				"target": "/path 4/",
-			},
-		},
-	}
-	s, err := LoadService("Test Name", m, ".", nil, true)
-	assert.NilError(t, err)
-	assert.Equal(t, len(s.Volumes), 2)
-	assert.Equal(t, "/path 1", s.Volumes[0].Target)
-	assert.Equal(t, "/path 2", s.Volumes[1].Target)
-	assert.Equal(t, len(s.Configs), 1)
-	assert.Equal(t, "/path 3", s.Configs[0].Target)
-	assert.Equal(t, len(s.Secrets), 1)
-	assert.Equal(t, "/path 4", s.Secrets[0].Target)
-}
diff --git a/loader/merge_test.go b/loader/merge_test.go
index b96fd33..42f5f38 100644
--- a/loader/merge_test.go
+++ b/loader/merge_test.go
@@ -1224,37 +1224,3 @@ func TestMergeCommands(t *testing.T) {
 	assert.NilError(t, err)
 	assert.DeepEqual(t, merged.Services[0].Command, types.ShellCommand{"/bin/ash", "-c", "echo 'world'"})
 }
-
-func TestMergeEnvironments(t *testing.T) {
-	configDetails := types.ConfigDetails{
-		ConfigFiles: []types.ConfigFile{
-			{Filename: "base.yml", Config: map[string]interface{}{
-				"services": map[string]interface{}{
-					"foo": map[string]interface{}{
-						"image": "alpine",
-						"environment": map[string]interface{}{
-							"NAME":  "BASE",
-							"VALUE": "BASE",
-						},
-					},
-				},
-			}},
-			{Filename: "override.yml", Config: map[string]interface{}{
-				"services": map[string]interface{}{
-					"foo": map[string]interface{}{
-						"image": "alpine",
-						"environment": map[string]interface{}{
-							"NAME":  "DEV",
-							"VALUE": nil,
-						},
-					},
-				},
-			}},
-		},
-	}
-	merged, err := loadTestProject(configDetails)
-	assert.NilError(t, err)
-	env := merged.Services[0].Environment
-	assert.Assert(t, *env["NAME"] == "DEV")
-	assert.Assert(t, env["VALUE"] == nil)
-}