From 473f54fdafe383a53ce34cd60f2594a7b54be090 Mon Sep 17 00:00:00 2001 From: AiYijing Date: Tue, 24 Aug 2021 10:09:34 +0800 Subject: [PATCH] Add support for windows volume (#1417) Signed-off-by: aiyijing --- pkg/transformer/utils.go | 89 ++++++++ pkg/transformer/utils_test.go | 199 ++++++++++++++++++ script/test/cmd/tests_new.sh | 9 + .../volume-mounts/windows/docker-compose.yaml | 8 + .../volume-mounts/windows/output-k8s.json | 115 ++++++++++ .../volume-mounts/windows/output-os.json | 174 +++++++++++++++ 6 files changed, 594 insertions(+) create mode 100644 script/test/fixtures/volume-mounts/windows/docker-compose.yaml create mode 100644 script/test/fixtures/volume-mounts/windows/output-k8s.json create mode 100644 script/test/fixtures/volume-mounts/windows/output-os.json 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) { 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": {} + } + ] +}