Repository: kubernetes/kompose
Pull request #1417 (2021-08-22 00:04:16)

Add parse windows volume
Add parse windows volume

Close https://github.com/kubernetes/kompose/issues/1368

Signed-off-by: aiyijing <aiyijing@live.com>

k8s-ci-robot: Welcome @aiyijing! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes/kompose'>kubernetes/kompose</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes/kompose has [its own contribution guidelines](https://github.com/kubernetes/kompose/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/kompose/pull/1417#" title="Author self-approved">aiyijing</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **janetkuo** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @janetkuo` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkompose).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/kompose/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["janetkuo"]} -->
aiyijing: /assign hangyan 
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/kompose/pull/1417#" title="Author self-approved">aiyijing</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **hangyan** after the PR has been reviewed.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkompose).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/kompose/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["hangyan"]} -->
hangyan: Code lgtm to me now, but a compose yaml test would be better

Patch:
diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go
index e7bf98cd..917e0115 100644
--- a/pkg/transformer/kubernetes/kubernetes.go
+++ b/pkg/transformer/kubernetes/kubernetes.go
@@ -185,6 +185,14 @@ func (k *Kubernetes) InitPodSpecWithConfigMap(name string, image string, service
 		},
 		Volumes: volumes,
 	}
+
+	if service.ImagePullSecret != "" {
+		pod.ImagePullSecrets = []api.LocalObjectReference{
+			{
+				Name: service.ImagePullSecret,
+			},
+		}
+	}
 	return pod
 }
 
diff --git a/pkg/transformer/utils.go b/pkg/transformer/utils.go
index 98ca4df6..731715ec 100644
--- a/pkg/transformer/utils.go
+++ b/pkg/transformer/utils.go
@@ -64,6 +64,13 @@ func CreateOutFile(out string) (*os.File, error) {
 
 // ParseVolume parses a given volume, which might be [name:][host:]container[:access_mode]
 func ParseVolume(volume string) (name, host, container, mode string, err error) {
+	if containWindowsPath(volume) {
+		return parseWindowsVolume(volume)
+	}
+	return parseVolume(volume)
+}
+
+func parseVolume(volume string) (name, host, container, mode string, err error) {
 	separator := ":"
 
 	// Parse based on ":"
@@ -112,6 +119,88 @@ func ParseVolume(volume string) (name, host, container, mode string, err error)
 	return
 }
 
+// parseVolume parses window volume.
+// example: windows host mount to windows container
+// volume = dataVolumeName:C:\Users\Data:D:\config:rw
+// it can be parsed:
+// name=dataVolumeName, host=C:\Users\Data, container=D:\config, mode=rw
+// example: windows host mount to linux container
+// volume = dataVolumeName:C:\Users\Data:/etc/config:rw
+// it can be parsed:
+// name=dataVolumeName, host=C:\Users\Data, container=/etc/config, mode=rw
+func parseWindowsVolume(volume string) (name, host, container, mode string, err error) {
+	var (
+		buffer, volumePaths []string
+		volumeStrings       = strings.Split(volume, ":")
+	)
+
+	// extract path and leave order
+	for _, fragment := range volumeStrings {
+		switch {
+		case containWindowsPath(fragment):
+			if len(buffer) == 0 {
+				err = fmt.Errorf("invalid windows volume %s", volume)
+				return
+			}
+
+			driveLetter := buffer[len(buffer)-1]
+			if len(driveLetter) != 1 {
+				err = fmt.Errorf("invalid windows volume %s", volume)
+				return
+			}
+			volumePaths = append(volumePaths, driveLetter+":"+fragment)
+			buffer = buffer[:len(buffer)-1]
+
+		case isPath(fragment):
+			volumePaths = append(volumePaths, fragment)
+		default:
+			buffer = append(buffer, fragment)
+		}
+	}
+
+	// set name and mode if exist
+	if len(buffer) == 1 {
+		if volumeStrings[0] == buffer[0] {
+			name = buffer[0]
+		} else if volumeStrings[len(volumeStrings)-1] == buffer[0] {
+			mode = buffer[0]
+		}
+	} else if len(buffer) == 2 {
+		name = buffer[0]
+		mode = buffer[1]
+	} else if len(buffer) > 2 {
+		err = fmt.Errorf("invalid windows volume %s", volume)
+		return
+	}
+
+	// Support in pass time
+	// Check to see if :Z or :z exists. We do not support SELinux relabeling at the moment.
+	// See https://github.com/kubernetes/kompose/issues/176
+	// Otherwise, check to see if "rw" or "ro" has been passed
+	if mode == "z" || mode == "Z" {
+		log.Warnf("Volume mount \"%s\" will be mounted without labeling support. :z or :Z not supported", volume)
+		mode = ""
+	}
+
+	// Set host and container if exist
+	if len(volumePaths) == 1 {
+		container = volumePaths[0]
+	} else if len(volumePaths) == 2 {
+		host = volumePaths[0]
+		container = volumePaths[1]
+	} else {
+		err = fmt.Errorf("invalid windows volume %s", volume)
+		return
+	}
+	return
+}
+
+// containWindowsPath check whether it contains windows path.
+// windows path's separator is "\"
+func containWindowsPath(substring string) bool {
+	return strings.Contains(substring, "\\")
+}
+
 // ParseIngressPath parse path for ingress.
 // eg. example.com/org -> example.com org
 func ParseIngressPath(url string) (string, string) {


Test patch:
diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go
index 2e4dd520..8209192b 100644
--- a/pkg/transformer/kubernetes/kubernetes_test.go
+++ b/pkg/transformer/kubernetes/kubernetes_test.go
@@ -23,6 +23,8 @@ import (
 	"strings"
 	"testing"
 
+	dockerCliTypes "github.com/docker/cli/cli/compose/types"
+
 	"github.com/kubernetes/kompose/pkg/kobject"
 	"github.com/kubernetes/kompose/pkg/loader/compose"
 	"github.com/kubernetes/kompose/pkg/transformer"
@@ -38,30 +40,33 @@ import (
 
 func newServiceConfig() kobject.ServiceConfig {
 	return kobject.ServiceConfig{
-		Name:          "app",
-		ContainerName: "name",
-		Image:         "image",
-		Environment:   []kobject.EnvVar{kobject.EnvVar{Name: "env", Value: "value"}},
-		Port:          []kobject.Ports{kobject.Ports{HostPort: 123, ContainerPort: 456}, kobject.Ports{HostPort: 123, ContainerPort: 456, Protocol: api.ProtocolUDP}},
-		Command:       []string{"cmd"},
-		WorkingDir:    "dir",
-		Args:          []string{"arg1", "arg2"},
-		VolList:       []string{"/tmp/volume"},
-		Network:       []string{"network1", "network2"}, // supported
-		Labels:        nil,
-		Annotations:   map[string]string{"abc": "def"},
-		CPUQuota:      1, // not supported
-		CapAdd:        []string{"cap_add"},
-		CapDrop:       []string{"cap_drop"},
-		Expose:        []string{"expose"}, // not supported
-		Privileged:    true,
-		Restart:       "always",
-		Stdin:         true,
-		Tty:           true,
-		TmpFs:         []string{"/tmp"},
-		Replicas:      2,
-		Volumes:       []kobject.Volumes{{SvcName: "app", MountPath: "/tmp/volume", PVCName: "app-claim0"}},
-		GroupAdd:      []int64{1003, 1005},
+		Name:            "app",
+		ContainerName:   "name",
+		Image:           "image",
+		Environment:     []kobject.EnvVar{kobject.EnvVar{Name: "env", Value: "value"}},
+		Port:            []kobject.Ports{kobject.Ports{HostPort: 123, ContainerPort: 456}, kobject.Ports{HostPort: 123, ContainerPort: 456, Protocol: api.ProtocolUDP}},
+		Command:         []string{"cmd"},
+		WorkingDir:      "dir",
+		Args:            []string{"arg1", "arg2"},
+		VolList:         []string{"/tmp/volume"},
+		Network:         []string{"network1", "network2"}, // supported
+		Labels:          nil,
+		Annotations:     map[string]string{"abc": "def"},
+		CPUQuota:        1, // not supported
+		CapAdd:          []string{"cap_add"},
+		CapDrop:         []string{"cap_drop"},
+		Expose:          []string{"expose"}, // not supported
+		Privileged:      true,
+		Restart:         "always",
+		ImagePullSecret: "regcred",
+		Stdin:           true,
+		Tty:             true,
+		TmpFs:           []string{"/tmp"},
+		Replicas:        2,
+		Volumes:         []kobject.Volumes{{SvcName: "app", MountPath: "/tmp/volume", PVCName: "app-claim0"}},
+		GroupAdd:        []int64{1003, 1005},
+		Configs:         []dockerCliTypes.ServiceConfigObjConfig{{Source: "config", Target: "/etc/world"}},
+		ConfigsMetaData: map[string]dockerCliTypes.ConfigObjConfig{"config": dockerCliTypes.ConfigObjConfig{Name: "myconfig", File: "kubernetes_test.go"}},
 	}
 }
 
@@ -163,7 +168,7 @@ func checkPodTemplate(config kobject.ServiceConfig, template api.PodTemplateSpec
 	if !equalStringSlice(config.Args, container.Args) {
 		return fmt.Errorf("Found different container args: %#v vs. %#v", config.Args, container.Args)
 	}
-	if len(template.Spec.Volumes) == 0 || len(template.Spec.Volumes[0].Name) == 0 || template.Spec.Volumes[0].VolumeSource.PersistentVolumeClaim == nil {
+	if len(template.Spec.Volumes) == 0 || len(template.Spec.Volumes[0].Name) == 0 || template.Spec.Volumes[0].VolumeSource.PersistentVolumeClaim == nil && template.Spec.Volumes[0].ConfigMap == nil {
 		return fmt.Errorf("Found incorrect volumes: %v vs. %#v", config.Volumes, template.Spec.Volumes)
 	}
 	// We only set controller labels here and k8s server will take care of other defaults, such as selectors
@@ -183,6 +188,9 @@ func checkPodTemplate(config kobject.ServiceConfig, template api.PodTemplateSpec
 	if config.Tty != template.Spec.Containers[0].TTY {
 		return fmt.Errorf("Found different values for TTY: %#v vs. %#v", config.Tty, template.Spec.Containers[0].TTY)
 	}
+	if config.ImagePullSecret != template.Spec.ImagePullSecrets[0].Name {
+		return fmt.Errorf("Found different values for ImagePullSecrets: %#v vs. %#v", config.ImagePullSecret, template.Spec.ImagePullSecrets[0].Name)
+	}
 	return nil
 }
 
@@ -281,12 +289,12 @@ func TestKomposeConvert(t *testing.T) {
 		expectedNumObjs int
 	}{
 		// objects generated are deployment, service nework policies (2) and pvc
-		"Convert to Deployments (D)":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, Replicas: replicas, IsReplicaSetFlag: true}, 5},
-		"Convert to Deployments (D) with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true}, 5},
-		"Convert to DaemonSets (DS)":                  {newKomposeObject(), kobject.ConvertOptions{CreateDS: true}, 5},
+		"Convert to Deployments (D)":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, Replicas: replicas, IsReplicaSetFlag: true}, 6},
+		"Convert to Deployments (D) with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true}, 6},
+		"Convert to DaemonSets (DS)":                  {newKomposeObject(), kobject.ConvertOptions{CreateDS: true}, 6},
 		// objects generated are deployment, daemonset, ReplicationController, service and pvc
-		"Convert to D, DS, and RC":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true, Replicas: replicas, IsReplicaSetFlag: true}, 6},
-		"Convert to D, DS, and RC with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true}, 6},
+		"Convert to D, DS, and RC":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true, Replicas: replicas, IsReplicaSetFlag: true}, 7},
+		"Convert to D, DS, and RC with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true}, 7},
 		// TODO: add more tests
 	}
 
@@ -578,21 +586,21 @@ func TestMultipleContainersInPod(t *testing.T) {
 					"app1": createConfig("app1", &containerName),
 					"app2": createConfig("app2", &containerName),
 				},
-			}, kobject.ConvertOptions{MultipleContainerMode: true}, 2, []string{"app1", "app2"}},
+			}, kobject.ConvertOptions{MultipleContainerMode: true}, 3, []string{"app1", "app2"}},
 		"Converted multiple containers to Deployments (D)": {
 			kobject.KomposeObject{
 				ServiceConfigs: map[string]kobject.ServiceConfig{
 					"app1": createConfig("app1", &containerName),
 					"app2": createConfig("app2", &containerName),
 				},
-			}, kobject.ConvertOptions{MultipleContainerMode: true, CreateD: true}, 3, []string{"app1", "app2"}},
+			}, kobject.ConvertOptions{MultipleContainerMode: true, CreateD: true}, 4, []string{"app1", "app2"}},
 		"Converted multiple containers (ContainerName are nil) to Deployments (D)": {
 			kobject.KomposeObject{
 				ServiceConfigs: map[string]kobject.ServiceConfig{
 					"app1": createConfig("app1", nil),
 					"app2": createConfig("app2", nil),
 				},
-			}, kobject.ConvertOptions{MultipleContainerMode: true, CreateD: true}, 3, []string{"name", "name"}},
+			}, kobject.ConvertOptions{MultipleContainerMode: true, CreateD: true}, 4, []string{"name", "name"}},
 		// TODO: add more tests
 	}
 
diff --git a/pkg/transformer/utils_test.go b/pkg/transformer/utils_test.go
index fec5c3ef..5c232719 100644
--- a/pkg/transformer/utils_test.go
+++ b/pkg/transformer/utils_test.go
@@ -34,6 +34,7 @@ func TestFormatProviderName(t *testing.T) {
 // When passing "z" or "Z" we expect "" back.
 func TestZParseVolumeLabeling(t *testing.T) {
 	testCase := "/foobar:/foobar:Z"
+	windowVolumeTestCase := "C:\\foobar:/foobar:Z"
 	_, _, _, mode, err := ParseVolume(testCase)
 	if err != nil {
 		t.Errorf("In test case %q, returned unexpected error %v", testCase, err)
@@ -41,6 +42,204 @@ func TestZParseVolumeLabeling(t *testing.T) {
 	if mode != "" {
 		t.Errorf("In test case %q, returned mode %s, expected \"\"", testCase, mode)
 	}
+
+	_, _, _, mode, err = ParseVolume(windowVolumeTestCase)
+	if err != nil {
+		t.Errorf("In test case %q, returned unexpected error %v", windowVolumeTestCase, err)
+	}
+	if mode != "" {
+		t.Errorf("In test case %q, returned mode %s, expected \"\"", windowVolumeTestCase, mode)
+	}
+}
+
+func TestParseWindowsVolumeMountLinuxContainer(t *testing.T) {
+	name := "datavolume"
+	windowsHosts := "C:\\Users"
+	linuxContainer := "/etc/configs/"
+	mode := "rw"
+
+	tests := []struct {
+		test, volume, name, host, container, mode string
+	}{
+		{
+			"name:host:container:mode",
+			fmt.Sprintf("%s:%s:%s:%s", name, windowsHosts, linuxContainer, mode),
+			name,
+			windowsHosts,
+			linuxContainer,
+			mode,
+		},
+		{
+			"host:container:mode",
+			fmt.Sprintf("%s:%s:%s", windowsHosts, linuxContainer, mode),
+			"",
+			windowsHosts,
+			linuxContainer,
+			mode,
+		},
+		{
+			"name:container:mode",
+			fmt.Sprintf("%s:%s:%s", name, linuxContainer, mode),
+			name,
+			"",
+			linuxContainer,
+			mode,
+		},
+		{
+			"name:host:container",
+			fmt.Sprintf("%s:%s:%s", name, windowsHosts, linuxContainer),
+			name,
+			windowsHosts,
+			linuxContainer,
+			"",
+		},
+		{
+			"host:container",
+			fmt.Sprintf("%s:%s", windowsHosts, linuxContainer),
+			"",
+			windowsHosts,
+			linuxContainer,
+			"",
+		},
+		{
+			"container:mode",
+			fmt.Sprintf("%s:%s", linuxContainer, mode),
+			"",
+			"",
+			linuxContainer,
+			mode,
+		},
+		{
+			"name:container",
+			fmt.Sprintf("%s:%s", name, linuxContainer),
+			name,
+			"",
+			linuxContainer,
+			"",
+		},
+		{
+			"container",
+			fmt.Sprintf("%s", linuxContainer),
+			"",
+			"",
+			linuxContainer,
+			"",
+		},
+	}
+
+	for _, test := range tests {
+		name, host, container, mode, err := ParseVolume(test.volume)
+		if err != nil {
+			t.Errorf("In test case %q, returned unexpected error %v", test.test, err)
+		}
+		if name != test.name {
+			t.Errorf("In test case %q, returned volume name %s, expected %s", test.test, name, test.name)
+		}
+		if host != test.host {
+			t.Errorf("In test case %q, returned host path %s, expected %s", test.test, host, test.host)
+		}
+		if container != test.container {
+			t.Errorf("In test case %q, returned container path %s, expected %s", test.test, container, test.container)
+		}
+		if mode != test.mode {
+			t.Errorf("In test case %q, returned access mode %s, expected %s", test.test, mode, test.mode)
+		}
+	}
+}
+
+func TestParseWindowsVolumeMountWindowsContainer(t *testing.T) {
+	name := "datavolume"
+	windowsHosts := "C:\\Users"
+	windowsContainer := "D:\\Users"
+	mode := "rw"
+
+	tests := []struct {
+		test, volume, name, host, container, mode string
+	}{
+		{
+			"name:host:container:mode",
+			fmt.Sprintf("%s:%s:%s:%s", name, windowsHosts, windowsContainer, mode),
+			name,
+			windowsHosts,
+			windowsContainer,
+			mode,
+		},
+		{
+			"host:container:mode",
+			fmt.Sprintf("%s:%s:%s", windowsHosts, windowsContainer, mode),
+			"",
+			windowsHosts,
+			windowsContainer,
+			mode,
+		},
+		{
+			"name:container:mode",
+			fmt.Sprintf("%s:%s:%s", name, windowsContainer, mode),
+			name,
+			"",
+			windowsContainer,
+			mode,
+		},
+		{
+			"name:host:container",
+			fmt.Sprintf("%s:%s:%s", name, windowsHosts, windowsContainer),
+			name,
+			windowsHosts,
+			windowsContainer,
+			"",
+		},
+		{
+			"host:container",
+			fmt.Sprintf("%s:%s", windowsHosts, windowsContainer),
+			"",
+			windowsHosts,
+			windowsContainer,
+			"",
+		},
+		{
+			"container:mode",
+			fmt.Sprintf("%s:%s", windowsContainer, mode),
+			"",
+			"",
+			windowsContainer,
+			mode,
+		},
+		{
+			"name:container",
+			fmt.Sprintf("%s:%s", name, windowsContainer),
+			name,
+			"",
+			windowsContainer,
+			"",
+		},
+		{
+			"container",
+			fmt.Sprintf("%s", windowsContainer),
+			"",
+			"",
+			windowsContainer,
+			"",
+		},
+	}
+
+	for _, test := range tests {
+		name, host, container, mode, err := ParseVolume(test.volume)
+		if err != nil {
+			t.Errorf("In test case %q, returned unexpected error %v", test.test, err)
+		}
+		if name != test.name {
+			t.Errorf("In test case %q, returned volume name %s, expected %s", test.test, name, test.name)
+		}
+		if host != test.host {
+			t.Errorf("In test case %q, returned host path %s, expected %s", test.test, host, test.host)
+		}
+		if container != test.container {
+			t.Errorf("In test case %q, returned container path %s, expected %s", test.test, container, test.container)
+		}
+		if mode != test.mode {
+			t.Errorf("In test case %q, returned access mode %s, expected %s", test.test, mode, test.mode)
+		}
+	}
 }
 
 func TestParseVolume(t *testing.T) {
diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh
index 0943a1bf..0aa3395d 100755
--- a/script/test/cmd/tests_new.sh
+++ b/script/test/cmd/tests_new.sh
@@ -106,3 +106,12 @@ convert::expect_warning "$cmd" "Push image registry 'whatever' is specified but
 #TEST the kompose.volume.storage-class-name label
 convert::check_artifacts_generated "kompose -f $KOMPOSE_ROOT/script/test/fixtures/storage-class-name/docker-compose.yml convert -o $TEMP_DIR/output-k8s.json -j" "$TEMP_DIR/output-k8s.json"
 convert::check_artifacts_generated "kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/storage-class-name/docker-compose.yml convert -o $TEMP_DIR/output-os.json -j" "$TEMP_DIR/output-os.json"
+
+# TEST the windows volume
+# windows host path to windows container
+k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/volume-mounts/windows/docker-compose.yaml convert --stdout -j --with-kompose-annotation=false"
+os_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/volume-mounts/windows/docker-compose.yaml convert --stdout -j --with-kompose-annotation=false"
+k8s_output="$KOMPOSE_ROOT/script/test/fixtures/volume-mounts/windows/output-k8s.json"
+os_output="$KOMPOSE_ROOT/script/test/fixtures/volume-mounts/windows/output-os.json"
+convert::expect_success "$k8s_cmd" "$k8s_output"
+convert::expect_success "$os_cmd" "$os_output"
diff --git a/script/test/fixtures/volume-mounts/windows/docker-compose.yaml b/script/test/fixtures/volume-mounts/windows/docker-compose.yaml
new file mode 100644
index 00000000..54246bd7
--- /dev/null
+++ b/script/test/fixtures/volume-mounts/windows/docker-compose.yaml
@@ -0,0 +1,8 @@
+version: '3.4'
+services:
+  db:
+    image: nginx:latest
+    ports:
+      - "80"
+    volumes:
+      - C:\Users\data_sux:D:\config:rw
diff --git a/script/test/fixtures/volume-mounts/windows/output-k8s.json b/script/test/fixtures/volume-mounts/windows/output-k8s.json
new file mode 100644
index 00000000..ed134219
--- /dev/null
+++ b/script/test/fixtures/volume-mounts/windows/output-k8s.json
@@ -0,0 +1,115 @@
+{
+  "kind": "List",
+  "apiVersion": "v1",
+  "metadata": {},
+  "items": [
+    {
+      "kind": "Service",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "ports": [
+          {
+            "name": "80",
+            "port": 80,
+            "targetPort": 80
+          }
+        ],
+        "selector": {
+          "io.kompose.service": "db"
+        }
+      },
+      "status": {
+        "loadBalancer": {}
+      }
+    },
+    {
+      "kind": "Deployment",
+      "apiVersion": "apps/v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "replicas": 1,
+        "selector": {
+          "matchLabels": {
+            "io.kompose.service": "db"
+          }
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "db"
+            }
+          },
+          "spec": {
+            "volumes": [
+              {
+                "name": "db-claim0",
+                "persistentVolumeClaim": {
+                  "claimName": "db-claim0"
+                }
+              }
+            ],
+            "containers": [
+              {
+                "name": "db",
+                "image": "nginx:latest",
+                "ports": [
+                  {
+                    "containerPort": 80
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "db-claim0",
+                    "mountPath": "D:\\config"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        },
+        "strategy": {
+          "type": "Recreate"
+        }
+      },
+      "status": {}
+    },
+    {
+      "kind": "PersistentVolumeClaim",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db-claim0",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db-claim0"
+        }
+      },
+      "spec": {
+        "accessModes": [
+          "ReadWriteOnce"
+        ],
+        "resources": {
+          "requests": {
+            "storage": "100Mi"
+          }
+        }
+      },
+      "status": {}
+    }
+  ]
+}
diff --git a/script/test/fixtures/volume-mounts/windows/output-os.json b/script/test/fixtures/volume-mounts/windows/output-os.json
new file mode 100644
index 00000000..80953ff7
--- /dev/null
+++ b/script/test/fixtures/volume-mounts/windows/output-os.json
@@ -0,0 +1,174 @@
+{
+  "kind": "List",
+  "apiVersion": "v1",
+  "metadata": {},
+  "items": [
+    {
+      "kind": "Service",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "ports": [
+          {
+            "name": "80",
+            "port": 80,
+            "targetPort": 80
+          }
+        ],
+        "selector": {
+          "io.kompose.service": "db"
+        }
+      },
+      "status": {
+        "loadBalancer": {}
+      }
+    },
+    {
+      "kind": "DeploymentConfig",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "strategy": {
+          "type": "Recreate",
+          "resources": {}
+        },
+        "triggers": [
+          {
+            "type": "ConfigChange"
+          },
+          {
+            "type": "ImageChange",
+            "imageChangeParams": {
+              "automatic": true,
+              "containerNames": [
+                "db"
+              ],
+              "from": {
+                "kind": "ImageStreamTag",
+                "name": "db:latest"
+              }
+            }
+          }
+        ],
+        "replicas": 1,
+        "test": false,
+        "selector": {
+          "io.kompose.service": "db"
+        },
+        "template": {
+          "metadata": {
+            "creationTimestamp": null,
+            "labels": {
+              "io.kompose.service": "db"
+            }
+          },
+          "spec": {
+            "volumes": [
+              {
+                "name": "db-claim0",
+                "persistentVolumeClaim": {
+                  "claimName": "db-claim0"
+                }
+              }
+            ],
+            "containers": [
+              {
+                "name": "db",
+                "image": " ",
+                "ports": [
+                  {
+                    "containerPort": 80
+                  }
+                ],
+                "resources": {},
+                "volumeMounts": [
+                  {
+                    "name": "db-claim0",
+                    "mountPath": "D:\\config"
+                  }
+                ]
+              }
+            ],
+            "restartPolicy": "Always"
+          }
+        }
+      },
+      "status": {
+        "latestVersion": 0,
+        "observedGeneration": 0,
+        "replicas": 0,
+        "updatedReplicas": 0,
+        "availableReplicas": 0,
+        "unavailableReplicas": 0
+      }
+    },
+    {
+      "kind": "ImageStream",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db"
+        }
+      },
+      "spec": {
+        "lookupPolicy": {
+          "local": false
+        },
+        "tags": [
+          {
+            "name": "",
+            "annotations": null,
+            "from": {
+              "kind": "DockerImage",
+              "name": "nginx:latest"
+            },
+            "generation": null,
+            "importPolicy": {},
+            "referencePolicy": {
+              "type": ""
+            }
+          }
+        ]
+      },
+      "status": {
+        "dockerImageRepository": ""
+      }
+    },
+    {
+      "kind": "PersistentVolumeClaim",
+      "apiVersion": "v1",
+      "metadata": {
+        "name": "db-claim0",
+        "creationTimestamp": null,
+        "labels": {
+          "io.kompose.service": "db-claim0"
+        }
+      },
+      "spec": {
+        "accessModes": [
+          "ReadWriteOnce"
+        ],
+        "resources": {
+          "requests": {
+            "storage": "100Mi"
+          }
+        }
+      },
+      "status": {}
+    }
+  ]
+}