Repository: compose-spec/compose-go
Pull request #132 (2021-04-27 09:41:39)

introduce CreateHostPath
introduce Bind option `CreateHostPath` and set automatically when service volume has been parsed from short syntax


Patch:
diff --git a/loader/volume.go b/loader/volume.go
index 4dea56f..173b76b 100644
--- a/loader/volume.go
+++ b/loader/volume.go
@@ -119,18 +119,26 @@ func isBindOption(option string) bool {
 }
 
 func populateType(volume *types.ServiceVolumeConfig) {
-	switch {
-	// Anonymous volume
-	case volume.Source == "":
-		volume.Type = string(types.VolumeTypeVolume)
-	case isFilePath(volume.Source):
-		volume.Type = string(types.VolumeTypeBind)
-	default:
-		volume.Type = string(types.VolumeTypeVolume)
+	if isFilePath(volume.Source) {
+		volume.Type = types.VolumeTypeBind
+		if volume.Bind == nil {
+			volume.Bind = &types.ServiceVolumeBind{}
+		}
+		// For backward compatibility with docker-compose legacy, using short notation involves
+		// bind will create missing host path
+		volume.Bind.CreateHostPath = true
+	} else {
+		volume.Type = types.VolumeTypeVolume
+		if volume.Volume == nil {
+			volume.Volume = &types.ServiceVolumeVolume{}
+		}
 	}
 }
 
 func isFilePath(source string) bool {
+	if source == "" {
+		return false
+	}
 	switch source[0] {
 	case '.', '/', '~':
 		return true
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 1dbda98..f7460bf 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -407,7 +407,8 @@
                   "bind": {
                     "type": "object",
                     "properties": {
-                      "propagation": {"type": "string"}
+                      "propagation": {"type": "string"},
+                      "create_host_path": {"type": "boolean"}
                     },
                     "additionalProperties": false,
                     "patternProperties": {"^x-": {}}
diff --git a/types/types.go b/types/types.go
index 752911a..c77132b 100644
--- a/types/types.go
+++ b/types/types.go
@@ -601,7 +601,8 @@ const (
 
 // ServiceVolumeBind are options for a service volume of type bind
 type ServiceVolumeBind struct {
-	Propagation string `yaml:",omitempty" json:"propagation,omitempty"`
+	Propagation    string `yaml:",omitempty" json:"propagation,omitempty"`
+	CreateHostPath bool   `mapstructure:"create_host_path" yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"`
 
 	Extensions map[string]interface{} `yaml:",inline" json:"-"`
 }


Test patch:
diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go
index 088b933..721ef60 100644
--- a/loader/full-struct_test.go
+++ b/loader/full-struct_test.go
@@ -394,12 +394,12 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
 			},
 			User: "someone",
 			Volumes: []types.ServiceVolumeConfig{
-				{Target: "/var/lib/mysql", Type: "volume"},
-				{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
-				{Source: workingDir, Target: "/code", Type: "bind"},
-				{Source: filepath.Join(workingDir, "static"), Target: "/var/www/html", Type: "bind"},
-				{Source: filepath.Join(homeDir, "/configs"), Target: "/etc/configs/", Type: "bind", ReadOnly: true},
-				{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
+				{Target: "/var/lib/mysql", Type: "volume", Volume: &types.ServiceVolumeVolume{}},
+				{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: "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{
 					Size: int64(10000),
@@ -820,22 +820,32 @@ func fullExampleYAML(workingDir, homeDir string) string {
     volumes:
     - type: volume
       target: /var/lib/mysql
+      volume: {}
     - type: bind
       source: /opt/data
       target: /var/lib/mysql
+      bind:
+        create_host_path: true
     - type: bind
       source: %s
       target: /code
+      bind:
+        create_host_path: true
     - type: bind
       source: %s
       target: /var/www/html
+      bind:
+        create_host_path: true
     - type: bind
       source: %s
       target: /etc/configs/
       read_only: true
+      bind:
+        create_host_path: true
     - type: volume
       source: datavolume
       target: /var/lib/mysql
+      volume: {}
     - type: bind
       source: %s
       target: /opt
@@ -1416,33 +1426,47 @@ func fullExampleJSON(workingDir, homeDir string) string {
       "volumes": [
         {
           "type": "volume",
-          "target": "/var/lib/mysql"
+          "target": "/var/lib/mysql",
+          "volume": {}
         },
         {
           "type": "bind",
           "source": "/opt/data",
-          "target": "/var/lib/mysql"
+          "target": "/var/lib/mysql",
+          "bind": {
+            "create_host_path": true
+          }
         },
         {
           "type": "bind",
           "source": "%s",
-          "target": "/code"
+          "target": "/code",
+          "bind": {
+            "create_host_path": true
+          }
         },
         {
           "type": "bind",
           "source": "%s",
-          "target": "/var/www/html"
+          "target": "/var/www/html",
+          "bind": {
+            "create_host_path": true
+          }
         },
         {
           "type": "bind",
           "source": "%s",
           "target": "/etc/configs/",
-          "read_only": true
+          "read_only": true,
+          "bind": {
+            "create_host_path": true
+          }
         },
         {
           "type": "volume",
           "source": "datavolume",
-          "target": "/var/lib/mysql"
+          "target": "/var/lib/mysql",
+          "volume": {}
         },
         {
           "type": "bind",
diff --git a/loader/volume_test.go b/loader/volume_test.go
index db2ce65..ca41867 100644
--- a/loader/volume_test.go
+++ b/loader/volume_test.go
@@ -28,7 +28,7 @@ import (
 func TestParseVolumeAnonymousVolume(t *testing.T) {
 	for _, path := range []string{"/path", "/path/foo"} {
 		volume, err := ParseVolume(path)
-		expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
+		expected := types.ServiceVolumeConfig{Type: "volume", Target: path, Volume: &types.ServiceVolumeVolume{}}
 		assert.NilError(t, err)
 		assert.Check(t, is.DeepEqual(expected, volume))
 	}
@@ -37,7 +37,7 @@ func TestParseVolumeAnonymousVolume(t *testing.T) {
 func TestParseVolumeAnonymousVolumeWindows(t *testing.T) {
 	for _, path := range []string{"C:\\path", "Z:\\path\\foo"} {
 		volume, err := ParseVolume(path)
-		expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
+		expected := types.ServiceVolumeConfig{Type: "volume", Target: path, Volume: &types.ServiceVolumeVolume{}}
 		assert.NilError(t, err)
 		assert.Check(t, is.DeepEqual(expected, volume))
 	}
@@ -71,6 +71,7 @@ func TestParseVolumeBindMount(t *testing.T) {
 			Type:   "bind",
 			Source: path,
 			Target: "/target",
+			Bind:   &types.ServiceVolumeBind{CreateHostPath: true},
 		}
 		assert.NilError(t, err)
 		assert.Check(t, is.DeepEqual(expected, volume))
@@ -89,6 +90,7 @@ func TestParseVolumeRelativeBindMountWindows(t *testing.T) {
 			Type:   "bind",
 			Source: path,
 			Target: "d:\\target",
+			Bind:   &types.ServiceVolumeBind{CreateHostPath: true},
 		}
 		assert.NilError(t, err)
 		assert.Check(t, is.DeepEqual(expected, volume))
@@ -101,7 +103,10 @@ func TestParseVolumeWithBindOptions(t *testing.T) {
 		Type:   "bind",
 		Source: "/source",
 		Target: "/target",
-		Bind:   &types.ServiceVolumeBind{Propagation: "slave"},
+		Bind: &types.ServiceVolumeBind{
+			CreateHostPath: true,
+			Propagation:    "slave",
+		},
 	}
 	assert.NilError(t, err)
 	assert.Check(t, is.DeepEqual(expected, volume))
@@ -114,7 +119,10 @@ func TestParseVolumeWithBindOptionsWindows(t *testing.T) {
 		Source:   "C:\\source\\foo",
 		Target:   "D:\\target",
 		ReadOnly: true,
-		Bind:     &types.ServiceVolumeBind{Propagation: "rprivate"},
+		Bind: &types.ServiceVolumeBind{
+			CreateHostPath: true,
+			Propagation:    "rprivate",
+		},
 	}
 	assert.NilError(t, err)
 	assert.Check(t, is.DeepEqual(expected, volume))
@@ -145,6 +153,7 @@ func TestParseVolumeWithReadOnly(t *testing.T) {
 			Source:   path,
 			Target:   "/target",
 			ReadOnly: true,
+			Bind:     &types.ServiceVolumeBind{CreateHostPath: true},
 		}
 		assert.NilError(t, err)
 		assert.Check(t, is.DeepEqual(expected, volume))
@@ -159,6 +168,7 @@ func TestParseVolumeWithRW(t *testing.T) {
 			Source:   path,
 			Target:   "/target",
 			ReadOnly: false,
+			Bind:     &types.ServiceVolumeBind{CreateHostPath: true},
 		}
 		assert.NilError(t, err)
 		assert.Check(t, is.DeepEqual(expected, volume))
@@ -172,6 +182,7 @@ func TestParseVolumeWindowsNamedPipe(t *testing.T) {
 		Type:   "bind",
 		Source: `\\.\pipe\docker_engine`,
 		Target: `\\.\pipe\inside`,
+		Bind:   &types.ServiceVolumeBind{CreateHostPath: true},
 	}
 	assert.Check(t, is.DeepEqual(expected, volume))
 }
diff --git a/loader/with-version-struct_test.go b/loader/with-version-struct_test.go
index a5a20c9..1ff23f9 100644
--- a/loader/with-version-struct_test.go
+++ b/loader/with-version-struct_test.go
@@ -50,7 +50,7 @@ func withVersionServices(workingDir, homeDir string) []types.ServiceConfig {
 			Command:     []string{"top"},
 			Environment: types.MappingWithEquals{},
 			Volumes: []types.ServiceVolumeConfig{
-				{Target: "/data", Type: "volume"},
+				{Target: "/data", Type: "volume", Volume: &types.ServiceVolumeVolume{}},
 			},
 		},
 	}