Repository: compose-spec/compose-go
Pull request #239 (2022-03-16 11:36:33)

Implement convert windows paths functionality
Implements [option to convert windows paths](https://docs.docker.com/compose/reference/envvars/#compose_convert_windows_paths) (was not present in v2).

Resolves docker/compose#9132


Patch:
diff --git a/cli/options.go b/cli/options.go
index 06446e6..65ec539 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -28,6 +28,7 @@ import (
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/loader"
 	"github.com/compose-spec/compose-go/types"
+	"github.com/compose-spec/compose-go/utils"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
@@ -332,7 +333,9 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
 		return nil, err
 	}
 
-	options.loadOptions = append(options.loadOptions, withNamePrecedenceLoad(absWorkingDir, options))
+	options.loadOptions = append(options.loadOptions,
+		withNamePrecedenceLoad(absWorkingDir, options),
+		withConvertWindowsPaths(options))
 
 	project, err := loader.Load(types.ConfigDetails{
 		ConfigFiles: configs,
@@ -359,6 +362,12 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
 	}
 }
 
+func withConvertWindowsPaths(options *ProjectOptions) func(*loader.Options) {
+	return func(o *loader.Options) {
+		o.ConvertWindowsPaths = utils.StringToBool(options.Environment["COMPOSE_CONVERT_WINDOWS_PATHS"])
+	}
+}
+
 // getConfigPathsFromOptions retrieves the config files for project based on project options
 func getConfigPathsFromOptions(options *ProjectOptions) ([]string, error) {
 	if len(options.ConfigPaths) != 0 {
diff --git a/loader/loader.go b/loader/loader.go
index 5aeeb21..9cc5233 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -53,6 +53,8 @@ type Options struct {
 	SkipNormalization bool
 	// Resolve paths
 	ResolvePaths bool
+	// Convert Windows paths
+	ConvertWindowsPaths bool
 	// Skip consistency check
 	SkipConsistencyCheck bool
 	// Skip extends
@@ -489,7 +491,7 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
 		return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, filename)
 	}
 
-	serviceConfig, err := LoadService(name, target.(map[string]interface{}), workingDir, lookupEnv, opts.ResolvePaths)
+	serviceConfig, err := LoadService(name, target.(map[string]interface{}), workingDir, lookupEnv, opts.ResolvePaths, opts.ConvertWindowsPaths)
 	if err != nil {
 		return nil, err
 	}
@@ -552,7 +554,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, resolvePaths bool) (*types.ServiceConfig, error) {
+func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, resolvePaths bool, convertPaths bool) (*types.ServiceConfig, error) {
 	serviceConfig := &types.ServiceConfig{
 		Scale: 1,
 	}
@@ -577,11 +579,30 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
 		if resolvePaths {
 			serviceConfig.Volumes[i] = resolveVolumePath(volume, workingDir, lookupEnv)
 		}
+
+		if convertPaths {
+			serviceConfig.Volumes[i] = convertVolumePath(volume)
+		}
 	}
 
 	return serviceConfig, nil
 }
 
+// Windows paths, c:\\my\\path\\shiny, need to be changed to be compatible with
+// the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
+func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConfig {
+	volumeName := strings.ToLower(filepath.VolumeName(volume.Source))
+	if len(volumeName) != 2 {
+		return volume
+	}
+
+	convertedSource := fmt.Sprintf("/%c%s", volumeName[0], volume.Source[len(volumeName):])
+	convertedSource = strings.ReplaceAll(convertedSource, "\\", "/")
+
+	volume.Source = convertedSource
+	return volume
+}
+
 func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
 	environment := types.MappingWithEquals{}
 
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index b208899..5bf4ddd 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -95,9 +95,7 @@
                 "labels": {"$ref": "#/definitions/list_or_dict"},
                 "cache_from": {"type": "array", "items": {"type": "string"}},
                 "cache_to": {"type": "array", "items": {"type": "string"}},
-                "no_cache": {"type": "boolean"},
                 "network": {"type": "string"},
-                "pull": {"type": "boolean"},
                 "target": {"type": "string"},
                 "shm_size": {"type": ["integer", "string"]},
                 "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
diff --git a/types/types.go b/types/types.go
index ec4b0bc..e25150e 100644
--- a/types/types.go
+++ b/types/types.go
@@ -298,8 +298,6 @@ type BuildConfig struct {
 	Labels     Labels            `yaml:",omitempty" json:"labels,omitempty"`
 	CacheFrom  StringList        `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
 	CacheTo    StringList        `mapstructure:"cache_to" yaml:"cache_to,omitempty" json:"cache_to,omitempty"`
-	NoCache    bool              `mapstructure:"no_cache" yaml:"no_cache,omitempty" json:"no_cache,omitempty"`
-	Pull       bool              `mapstructure:"pull" yaml:"pull,omitempty" json:"pull,omitempty"`
 	ExtraHosts HostsList         `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
 	Isolation  string            `yaml:",omitempty" json:"isolation,omitempty"`
 	Network    string            `yaml:",omitempty" json:"network,omitempty"`


Test patch:
diff --git a/cli/options_windows_test.go b/cli/options_windows_test.go
new file mode 100644
index 0000000..758a523
--- /dev/null
+++ b/cli/options_windows_test.go
@@ -0,0 +1,35 @@
+/*
+   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 cli
+
+import (
+	"os"
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+func TestConvertWithEnvVar(t *testing.T) {
+	os.Setenv("COMPOSE_CONVERT_WINDOWS_PATHS", "1")
+	defer os.Unsetenv("COMPOSE_CONVERT_WINDOWS_PATHS")
+	opts, _ := NewProjectOptions([]string{"testdata/simple/compose-with-paths.yaml"}, WithOsEnv)
+
+	p, err := ProjectFromOptions(opts)
+
+	assert.NilError(t, err)
+	assert.Equal(t, p.Services[0].Volumes[0].Source, "/c/docker/project")
+}
diff --git a/cli/testdata/simple/compose-with-paths.yaml b/cli/testdata/simple/compose-with-paths.yaml
new file mode 100644
index 0000000..7bd05a6
--- /dev/null
+++ b/cli/testdata/simple/compose-with-paths.yaml
@@ -0,0 +1,7 @@
+services:
+  test:
+    image: hello-world
+    volumes:
+    - type: bind
+      source: C:\docker\project
+      target: /test
diff --git a/loader/loader_test.go b/loader/loader_test.go
index a4106c8..961baef 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -1833,7 +1833,7 @@ func TestLoadServiceWithEnvFile(t *testing.T) {
 	s, err := LoadService("Test Name", m, ".", func(s string) (string, bool) {
 		assert.Equal(t, "TEST", s)
 		return "YES", true
-	}, true)
+	}, true, false)
 	assert.NilError(t, err)
 	assert.Equal(t, "YES", *s.Environment["HALLO"])
 }
@@ -1857,7 +1857,7 @@ func TestLoadServiceWithVolumes(t *testing.T) {
 			},
 		},
 	}
-	s, err := LoadService("Test Name", m, ".", nil, true)
+	s, err := LoadService("Test Name", m, ".", nil, true, false)
 	assert.NilError(t, err)
 	assert.Equal(t, len(s.Volumes), 2)
 	assert.Equal(t, "/path 1", s.Volumes[0].Target)
diff --git a/loader/loader_windows_test.go b/loader/loader_windows_test.go
new file mode 100644
index 0000000..dfceee5
--- /dev/null
+++ b/loader/loader_windows_test.go
@@ -0,0 +1,53 @@
+/*
+   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 TestConvertWindowsVolumePath(t *testing.T) {
+	var testcases = []struct {
+		windowsPath           string
+		expectedConvertedPath string
+	}{
+		{
+			windowsPath:           "c:\\hello\\docker",
+			expectedConvertedPath: "/c/hello/docker",
+		},
+		{
+			windowsPath:           "d:\\compose",
+			expectedConvertedPath: "/d/compose",
+		},
+		{
+			windowsPath:           "e:\\path with spaces\\compose",
+			expectedConvertedPath: "/e/path with spaces/compose",
+		},
+	}
+	for _, testcase := range testcases {
+		volume := types.ServiceVolumeConfig{
+			Type:   "bind",
+			Source: testcase.windowsPath,
+			Target: "/test",
+		}
+
+		assert.Equal(t, testcase.expectedConvertedPath, convertVolumePath(volume).Source)
+	}
+}