Repository: compose-spec/compose-go
Pull request #178 (2021-09-03 08:10:42)

introduce WithResolvedPaths to let client code opt for relative or abs
Add a load option to let client code decide if paths should be resolved (absolute) or kept relative.
This addresses https://github.com/docker/compose/issues/8503


Patch:
diff --git a/cli/options.go b/cli/options.go
index 28e8587..ec45d37 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -154,14 +154,6 @@ func WithDiscardEnvFile(o *ProjectOptions) error {
 	return nil
 }
 
-// WithLoadOptions provides a hook to control how compose files are loaded
-func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn {
-	return func(o *ProjectOptions) error {
-		o.loadOptions = append(o.loadOptions, loadOptions...)
-		return nil
-	}
-}
-
 // WithOsEnv imports environment variables from OS
 func WithOsEnv(o *ProjectOptions) error {
 	for k, v := range getAsEqualsMap(os.Environ()) {
@@ -248,6 +240,16 @@ func WithInterpolation(interpolation bool) ProjectOptionsFn {
 	}
 }
 
+// WithResolvedPaths set ProjectOptions to enable paths resolution
+func WithResolvedPaths(resolve bool) ProjectOptionsFn {
+	return func(o *ProjectOptions) error {
+		o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
+			options.ResolvePaths = resolve
+		})
+		return nil
+	}
+}
+
 // DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
 var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"}
 
@@ -322,7 +324,7 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
 		} else if nameFromEnv, ok := options.Environment[ComposeProjectName]; ok && nameFromEnv != "" {
 			opts.Name = nameFromEnv
 		} else {
-			opts.Name = regexp.MustCompile(`(?m)[a-z]+[-_a-z0-9]*`).FindString(strings.ToLower(filepath.Base(absWorkingDir)))
+			opts.Name = regexp.MustCompile(`(?m)[a-z]+[-_a-z0-9]*[a-z0-9]+`).FindString(strings.ToLower(filepath.Base(absWorkingDir)))
 		}
 		opts.Name = strings.ToLower(opts.Name)
 	}
diff --git a/loader/interpolate.go b/loader/interpolate.go
index 5cfa84b..b45172a 100644
--- a/loader/interpolate.go
+++ b/loader/interpolate.go
@@ -21,21 +21,14 @@ import (
 	"strings"
 
 	interp "github.com/compose-spec/compose-go/interpolation"
-	"github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
 )
 
 var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
 	servicePath("configs", interp.PathMatchList, "mode"):             toInt,
-	servicePath("cpu_count"):                                         toInt64,
-	servicePath("cpu_percent"):                                       toFloat,
-	servicePath("cpu_period"):                                        toInt64,
-	servicePath("cpu_quota"):                                         toInt64,
-	servicePath("cpu_rt_period"):                                     toInt64,
-	servicePath("cpu_rt_runtime"):                                    toInt64,
-	servicePath("cpus"):                                              toFloat32,
-	servicePath("cpu_shares"):                                        toInt64,
-	servicePath("init"):                                              toBoolean,
+	servicePath("secrets", interp.PathMatchList, "mode"):             toInt,
+	servicePath("healthcheck", "retries"):                            toInt,
+	servicePath("healthcheck", "disable"):                            toBoolean,
 	servicePath("deploy", "replicas"):                                toInt,
 	servicePath("deploy", "update_config", "parallelism"):            toInt,
 	servicePath("deploy", "update_config", "max_failure_ratio"):      toFloat,
@@ -43,28 +36,15 @@ var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
 	servicePath("deploy", "rollback_config", "max_failure_ratio"):    toFloat,
 	servicePath("deploy", "restart_policy", "max_attempts"):          toInt,
 	servicePath("deploy", "placement", "max_replicas_per_node"):      toInt,
-	servicePath("healthcheck", "retries"):                            toInt,
-	servicePath("healthcheck", "disable"):                            toBoolean,
-	servicePath("mem_limit"):                                         toUnitBytes,
-	servicePath("mem_reservation"):                                   toUnitBytes,
-	servicePath("memswap_limit"):                                     toUnitBytes,
-	servicePath("mem_swappiness"):                                    toUnitBytes,
-	servicePath("oom_kill_disable"):                                  toBoolean,
-	servicePath("oom_score_adj"):                                     toInt64,
-	servicePath("pids_limit"):                                        toInt64,
 	servicePath("ports", interp.PathMatchList, "target"):             toInt,
 	servicePath("ports", interp.PathMatchList, "published"):          toInt,
+	servicePath("ulimits", interp.PathMatchAll):                      toInt,
+	servicePath("ulimits", interp.PathMatchAll, "hard"):              toInt,
+	servicePath("ulimits", interp.PathMatchAll, "soft"):              toInt,
 	servicePath("privileged"):                                        toBoolean,
 	servicePath("read_only"):                                         toBoolean,
-	servicePath("scale"):                                             toInt,
-	servicePath("secrets", interp.PathMatchList, "mode"):             toInt,
-	servicePath("shm_size"):                                          toUnitBytes,
 	servicePath("stdin_open"):                                        toBoolean,
-	servicePath("stop_grace_period"):                                 toDuration,
 	servicePath("tty"):                                               toBoolean,
-	servicePath("ulimits", interp.PathMatchAll):                      toInt,
-	servicePath("ulimits", interp.PathMatchAll, "hard"):              toInt,
-	servicePath("ulimits", interp.PathMatchAll, "soft"):              toInt,
 	servicePath("volumes", interp.PathMatchList, "read_only"):        toBoolean,
 	servicePath("volumes", interp.PathMatchList, "volume", "nocopy"): toBoolean,
 	iPath("networks", interp.PathMatchAll, "external"):               toBoolean,
@@ -87,38 +67,10 @@ func toInt(value string) (interface{}, error) {
 	return strconv.Atoi(value)
 }
 
-func toInt64(value string) (interface{}, error) {
-	return strconv.ParseInt(value, 10, 64)
-}
-
-func toUnitBytes(value string) (interface{}, error) {
-	i, err := strconv.ParseInt(value, 10, 64)
-	if err != nil {
-		return nil, err
-	}
-	return types.UnitBytes(i), nil
-}
-
-func toDuration(value string) (interface{}, error) {
-	i, err := strconv.ParseInt(value, 10, 64)
-	if err != nil {
-		return nil, err
-	}
-	return types.Duration(i), nil
-}
-
 func toFloat(value string) (interface{}, error) {
 	return strconv.ParseFloat(value, 64)
 }
 
-func toFloat32(value string) (interface{}, error) {
-	f, err := strconv.ParseFloat(value, 32)
-	if err != nil {
-		return nil, err
-	}
-	return float32(f), nil
-}
-
 // should match http://yaml.org/type/bool.html
 func toBoolean(value string) (interface{}, error) {
 	switch strings.ToLower(value) {
diff --git a/loader/loader.go b/loader/loader.go
index 91c6923..935767d 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -23,6 +23,7 @@ import (
 	"path"
 	"path/filepath"
 	"reflect"
+	"regexp"
 	"sort"
 	"strings"
 	"time"
@@ -48,6 +49,8 @@ type Options struct {
 	SkipInterpolation bool
 	// Skip normalization
 	SkipNormalization bool
+	// Resolve paths
+	ResolvePaths bool
 	// Skip consistency check
 	SkipConsistencyCheck bool
 	// Skip extends
@@ -102,11 +105,6 @@ func WithDiscardEnvFiles(opts *Options) {
 	opts.discardEnvFiles = true
 }
 
-// WithSkipValidation sets the Options to skip validation when loading sections
-func WithSkipValidation(opts *Options) {
-	opts.SkipValidation = true
-}
-
 // ParseYAML reads the bytes from a file, parses the bytes into a mapping
 // structure, and returns it.
 func ParseYAML(source []byte) (map[string]interface{}, error) {
@@ -203,7 +201,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 	}
 
 	if !opts.SkipNormalization {
-		err = normalize(project)
+		err = normalize(project, opts.ResolvePaths)
 		if err != nil {
 			return nil, err
 		}
@@ -220,14 +218,21 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 }
 
 func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) {
-	yaml, err := ParseYAML(b)
-	if err != nil {
-		return nil, err
-	}
 	if !opts.SkipInterpolation {
-		return interp.Interpolate(yaml, *opts.Interpolate)
+		withoutFullLineComments := removeYamlComments(string(b))
+		substituted, err := opts.Interpolate.Substitute(withoutFullLineComments, template.Mapping(opts.Interpolate.LookupValue))
+		if err != nil {
+			return nil, err
+		}
+		b = []byte(substituted)
 	}
-	return yaml, err
+
+	return ParseYAML(b)
+}
+
+// removeYamlComments drop all full line comments from the yaml file, so we don't try to apply string substitutions on most of the irrelevant places
+func removeYamlComments(content string) string {
+	return regexp.MustCompile(`(?m)^[ \t]*#.*\n*`).ReplaceAllString(content, "")
 }
 
 func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} {
@@ -258,7 +263,7 @@ func loadSections(filename string, config map[string]interface{}, configDetails
 		return nil, err
 	}
 
-	cfg.Networks, err = LoadNetworks(getSection(config, "networks"))
+	cfg.Networks, err = LoadNetworks(getSection(config, "networks"), configDetails.Version)
 	if err != nil {
 		return nil, err
 	}
@@ -266,11 +271,11 @@ func loadSections(filename string, config map[string]interface{}, configDetails
 	if err != nil {
 		return nil, err
 	}
-	cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails)
+	cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails, opts.ResolvePaths)
 	if err != nil {
 		return nil, err
 	}
-	cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails)
+	cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails, opts.ResolvePaths)
 	if err != nil {
 		return nil, err
 	}
@@ -423,14 +428,6 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
 func LoadServices(filename string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options) ([]types.ServiceConfig, error) {
 	var services []types.ServiceConfig
 
-	x, ok := servicesDict["extensions"]
-	if ok {
-		// as a top-level attribute, "services" doesn't support extensions, and a service can be named `x-foo`
-		for k, v := range x.(map[string]interface{}) {
-			servicesDict[k] = v
-		}
-	}
-
 	for name := range servicesDict {
 		serviceConfig, err := loadServiceWithExtends(filename, name, servicesDict, workingDir, lookupEnv, opts, &cycleTracker{})
 		if err != nil {
@@ -448,7 +445,7 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
 		return nil, err
 	}
 
-	serviceConfig, err := LoadService(name, servicesDict[name].(map[string]interface{}), workingDir, lookupEnv)
+	serviceConfig, err := LoadService(name, servicesDict[name].(map[string]interface{}), workingDir, lookupEnv, opts.ResolvePaths)
 	if err != nil {
 		return nil, err
 	}
@@ -519,7 +516,7 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
 
 // LoadService produces a single ServiceConfig from a compose file Dict
 // the serviceDict is not validated if directly used. Use Load() to enable validation
-func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
+func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, resolvePaths bool) (*types.ServiceConfig, error) {
 	serviceConfig := &types.ServiceConfig{}
 	if err := Transform(serviceDict, serviceConfig); err != nil {
 		return nil, err
@@ -530,8 +527,18 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
 		return nil, err
 	}
 
-	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
-		return nil, err
+	for i, volume := range serviceConfig.Volumes {
+		if volume.Type != "bind" {
+			continue
+		}
+
+		if volume.Source == "" {
+			return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`)
+		}
+
+		if resolvePaths {
+			serviceConfig.Volumes[i] = resolveVolumePath(volume, workingDir, lookupEnv)
+		}
 	}
 
 	return serviceConfig, nil
@@ -566,30 +573,19 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
 	return nil
 }
 
-func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
-	for i, volume := range volumes {
-		if volume.Type != "bind" {
-			continue
-		}
-
-		if volume.Source == "" {
-			return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
-		}
-
-		filePath := expandUser(volume.Source, lookupEnv)
-		// Check if source is an absolute path (either Unix or Windows), to
-		// handle a Windows client with a Unix daemon or vice-versa.
-		//
-		// Note that this is not required for Docker for Windows when specifying
-		// a local Windows path, because Docker for Windows translates the Windows
-		// path into a valid path within the VM.
-		if !path.IsAbs(filePath) && !isAbs(filePath) {
-			filePath = absPath(workingDir, filePath)
-		}
-		volume.Source = filePath
-		volumes[i] = volume
-	}
-	return nil
+func resolveVolumePath(volume types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) types.ServiceVolumeConfig {
+	filePath := expandUser(volume.Source, lookupEnv)
+	// Check if source is an absolute path (either Unix or Windows), to
+	// handle a Windows client with a Unix daemon or vice-versa.
+	//
+	// Note that this is not required for Docker for Windows when specifying
+	// a local Windows path, because Docker for Windows translates the Windows
+	// path into a valid path within the VM.
+	if !path.IsAbs(filePath) && !isAbs(filePath) {
+		filePath = absPath(workingDir, filePath)
+	}
+	volume.Source = filePath
+	return volume
 }
 
 // TODO: make this more robust
@@ -625,7 +621,7 @@ func transformUlimits(data interface{}) (interface{}, error) {
 
 // LoadNetworks produces a NetworkConfig map from a compose file Dict
 // the source Dict is not validated if directly used. Use Load() to enable validation
-func LoadNetworks(source map[string]interface{}) (map[string]types.NetworkConfig, error) {
+func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) {
 	networks := make(map[string]types.NetworkConfig)
 	err := Transform(source, &networks)
 	if err != nil {
@@ -693,13 +689,13 @@ func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig,
 
 // LoadSecrets produces a SecretConfig map from a compose file Dict
 // the source Dict is not validated if directly used. Use Load() to enable validation
-func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
+func LoadSecrets(source map[string]interface{}, details types.ConfigDetails, resolvePaths bool) (map[string]types.SecretConfig, error) {
 	secrets := make(map[string]types.SecretConfig)
 	if err := Transform(source, &secrets); err != nil {
 		return secrets, err
 	}
 	for name, secret := range secrets {
-		obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
+		obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details, resolvePaths)
 		if err != nil {
 			return nil, err
 		}
@@ -711,13 +707,13 @@ func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (ma
 
 // LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
 // the source Dict is not validated if directly used. Use Load() to enable validation
-func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
+func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails, resolvePaths bool) (map[string]types.ConfigObjConfig, error) {
 	configs := make(map[string]types.ConfigObjConfig)
 	if err := Transform(source, &configs); err != nil {
 		return configs, err
 	}
 	for name, config := range configs {
-		obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
+		obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details, resolvePaths)
 		if err != nil {
 			return nil, err
 		}
@@ -727,7 +723,7 @@ func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails)
 	return configs, nil
 }
 
-func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
+func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails, resolvePaths bool) (types.FileObjectConfig, error) {
 	// if "external: true"
 	switch {
 	case obj.External.External:
@@ -750,7 +746,9 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi
 			return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
 		}
 	default:
-		obj.File = absPath(details.WorkingDir, obj.File)
+		if resolvePaths {
+			obj.File = absPath(details.WorkingDir, obj.File)
+		}
 	}
 
 	return obj, nil
diff --git a/loader/normalize.go b/loader/normalize.go
index fd527b7..30e9d20 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -28,7 +28,7 @@ import (
 )
 
 // normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
-func normalize(project *types.Project) error {
+func normalize(project *types.Project, resolvePaths bool) error {
 	absWorkingDir, err := filepath.Abs(project.WorkingDir)
 	if err != nil {
 		return err
@@ -72,8 +72,10 @@ func normalize(project *types.Project) error {
 			}
 			localContext := absPath(project.WorkingDir, s.Build.Context)
 			if _, err := os.Stat(localContext); err == nil {
-				s.Build.Context = localContext
-				s.Build.Dockerfile = absPath(localContext, s.Build.Dockerfile)
+				if resolvePaths {
+					s.Build.Context = localContext
+					s.Build.Dockerfile = absPath(localContext, s.Build.Dockerfile)
+				}
 			} else {
 				// might be a remote http/git context. Unfortunately supported "remote" syntax is highly ambiguous
 				// in moby/moby and not defined by compose-spec, so let's assume runtime will check
diff --git a/loader/validate.go b/loader/validate.go
index 28ecfa7..b853d17 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -38,15 +38,15 @@ func checkConsistency(project *types.Project) error {
 			}
 		}
 
-		if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
-			serviceName := s.NetworkMode[len(types.ServicePrefix):]
+		if strings.HasPrefix(s.NetworkMode, types.NetworkModeServicePrefix) {
+			serviceName := s.NetworkMode[len(types.NetworkModeServicePrefix):]
 			if _, err := project.GetServices(serviceName); err != nil {
 				return fmt.Errorf("service %q not found for network_mode 'service:%s'", serviceName, serviceName)
 			}
 		}
 
-		if strings.HasPrefix(s.NetworkMode, types.ContainerPrefix) {
-			containerName := s.NetworkMode[len(types.ContainerPrefix):]
+		if strings.HasPrefix(s.NetworkMode, types.NetworkModeContainerPrefix) {
+			containerName := s.NetworkMode[len(types.NetworkModeContainerPrefix):]
 			if _, err := project.GetByContainerName(containerName); err != nil {
 				return fmt.Errorf("service with container_name %q not found for network_mode 'container:%s'", containerName, containerName)
 			}
diff --git a/types/project.go b/types/project.go
index e8558ec..3a65eef 100644
--- a/types/project.go
+++ b/types/project.go
@@ -228,11 +228,6 @@ func (s Services) GetProfiles() []string {
 
 // ApplyProfiles disables service which don't match selected profiles
 func (p *Project) ApplyProfiles(profiles []string) {
-	for _, p := range profiles {
-		if p == "*" {
-			return
-		}
-	}
 	var enabled, disabled Services
 	for _, service := range p.Services {
 		if service.HasProfile(profiles) {
diff --git a/types/types.go b/types/types.go
index f4d5886..37238dd 100644
--- a/types/types.go
+++ b/types/types.go
@@ -226,17 +226,10 @@ const (
 )
 
 const (
-	// ServicePrefix is the prefix for references pointing to a service
-	ServicePrefix = "service:"
-	// ContainerPrefix is the prefix for references pointing to a container
-	ContainerPrefix = "container:"
-
 	// NetworkModeServicePrefix is the prefix for network_mode pointing to a service
-	// Deprecated prefer ServicePrefix
-	NetworkModeServicePrefix = ServicePrefix
+	NetworkModeServicePrefix = "service:"
 	// NetworkModeContainerPrefix is the prefix for network_mode pointing to a container
-	// Deprecated prefer ContainerPrefix
-	NetworkModeContainerPrefix = ContainerPrefix
+	NetworkModeContainerPrefix = "container:"
 )
 
 // GetDependencies retrieve all services this service depends on
@@ -253,21 +246,9 @@ func (s ServiceConfig) GetDependencies() []string {
 			dependencies.append(link)
 		}
 	}
-	if strings.HasPrefix(s.NetworkMode, ServicePrefix) {
-		dependencies.append(s.NetworkMode[len(ServicePrefix):])
-	}
-	if strings.HasPrefix(s.Ipc, ServicePrefix) {
-		dependencies.append(s.Ipc[len(ServicePrefix):])
+	if strings.HasPrefix(s.NetworkMode, NetworkModeServicePrefix) {
+		dependencies.append(s.NetworkMode[len(NetworkModeServicePrefix):])
 	}
-	if strings.HasPrefix(s.Pid, ServicePrefix) {
-		dependencies.append(s.Pid[len(ServicePrefix):])
-	}
-	for _, vol := range s.VolumesFrom {
-		if !strings.HasPrefix(s.Pid, ContainerPrefix) {
-			dependencies.append(vol)
-		}
-	}
-
 	return dependencies.toSlice()
 }
 
@@ -371,7 +352,7 @@ func (e MappingWithEquals) OverrideBy(other MappingWithEquals) MappingWithEquals
 // Resolve update a MappingWithEquals for keys without value (`key`, but not `key=`)
 func (e MappingWithEquals) Resolve(lookupFn func(string) (string, bool)) MappingWithEquals {
 	for k, v := range e {
-		if v == nil {
+		if v == nil || *v == "" {
 			if value, ok := lookupFn(k); ok {
 				e[k] = &value
 			}


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index dcf0b86..7129c6a 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -55,6 +55,7 @@ func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Project, error)
 	return Load(buildConfigDetails(yaml, env), func(options *Options) {
 		options.SkipConsistencyCheck = true
 		options.SkipNormalization = true
+		options.ResolvePaths = true
 	})
 }
 
@@ -446,29 +447,26 @@ services:
     environment:
       FOO: "1"
       BAR: 2
-      GA: 2.5
-      BU: ""
-      ZO:
-      MEU:
+      BAZ: 2.5
+      QUX:
+      QUUX:
   list-env:
     image: busybox
     environment:
       - FOO=1
       - BAR=2
-      - GA=2.5
-      - BU=
-      - ZO
-      - MEU
-`, map[string]string{"MEU": "Shadoks"})
+      - BAZ=2.5
+      - QUX=
+      - QUUX
+`, map[string]string{"QUX": "qux"})
 	assert.NilError(t, err)
 
 	expected := types.MappingWithEquals{
-		"FOO": strPtr("1"),
-		"BAR": strPtr("2"),
-		"GA":  strPtr("2.5"),
-		"BU":  strPtr(""),
-		"ZO":  nil,
-		"MEU": strPtr("Shadoks"),
+		"FOO":  strPtr("1"),
+		"BAR":  strPtr("2"),
+		"BAZ":  strPtr("2.5"),
+		"QUX":  strPtr("qux"),
+		"QUUX": nil,
 	}
 
 	assert.Check(t, is.Equal(2, len(config.Services)))
@@ -1342,7 +1340,7 @@ func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
 		},
 	}
 	details := types.ConfigDetails{}
-	secrets, err := LoadSecrets(source, details)
+	secrets, err := LoadSecrets(source, details, true)
 	assert.NilError(t, err)
 	expected := map[string]types.SecretConfig{
 		"foo": {
@@ -1365,7 +1363,7 @@ func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
 			},
 		},
 	}
-	networks, err := LoadNetworks(source)
+	networks, err := LoadNetworks(source, "3.5")
 	assert.NilError(t, err)
 	expected := map[string]types.NetworkConfig{
 		"foo": {
@@ -1389,7 +1387,7 @@ func TestLoadNetworksWarnOnDeprecatedExternalName(t *testing.T) {
 			},
 		},
 	}
-	networks, err := LoadNetworks(source)
+	networks, err := LoadNetworks(source, "3.4")
 	assert.NilError(t, err)
 	expected := map[string]types.NetworkConfig{
 		"foo": {
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index 3143271..2f500e6 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -19,7 +19,6 @@ package loader
 import (
 	"os"
 	"path/filepath"
-	"strings"
 	"testing"
 
 	"github.com/compose-spec/compose-go/types"
@@ -59,11 +58,11 @@ func TestNormalizeNetworkNames(t *testing.T) {
 		},
 	}
 
-	expected := strings.ReplaceAll(strings.ReplaceAll(`services:
+	expected := `services:
   foo:
     build:
-      context: /some/path/testdata
-      dockerfile: /some/path/testdata/Dockerfile
+      context: ./testdata
+      dockerfile: Dockerfile
       args:
         FOO: BAR
         ZOT: null
@@ -79,8 +78,8 @@ networks:
     name: CustomName
   mynet:
     name: myProject_mynet
-`, "/some/path", wd), "/", string(filepath.Separator))
-	err := normalize(&project)
+`
+	err := normalize(&project, false)
 	assert.NilError(t, err)
 	marshal, err := yaml.Marshal(project)
 	assert.NilError(t, err)
@@ -104,7 +103,7 @@ func TestNormalizeAbsolutePaths(t *testing.T) {
 		WorkingDir:   absWorkingDir,
 		ComposeFiles: []string{absComposeFile, absOverrideFile},
 	}
-	err := normalize(&project)
+	err := normalize(&project, false)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, expected, project)
 }
@@ -142,7 +141,7 @@ func TestNormalizeVolumes(t *testing.T) {
 		WorkingDir:   absCwd,
 		ComposeFiles: []string{},
 	}
-	err := normalize(&project)
+	err := normalize(&project, false)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, expected, project)
 }