Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Windows volume/mount support #13908

Merged
merged 2 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/podman/common/create_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c
cliOpts.Volume = append(cliOpts.Volume, vol)
// Extract the destination so we don't add duplicate mounts in
// the volumes phase.
splitVol := strings.SplitN(vol, ":", 3)
splitVol := specgen.SplitVolumeString(vol)
switch len(splitVol) {
case 1:
volDestinations[vol] = true
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/containernetworking/cni v1.0.1
github.com/containernetworking/plugins v1.1.1
github.com/containers/buildah v1.25.2-0.20220406205807-5b8e79118057
github.com/containers/common v0.47.5-0.20220421111103-112a47964ddb
github.com/containers/common v0.47.5-0.20220425182415-4081e6be9356
github.com/containers/conmon v2.0.20+incompatible
github.com/containers/image/v5 v5.21.1-0.20220421124950-8527e238867c
github.com/containers/ocicrypt v1.1.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19
github.com/containers/buildah v1.25.2-0.20220406205807-5b8e79118057 h1:lKSxhMBpcHyyQrj2QJYzcm56uiSeibRdSL2KoppF6rg=
github.com/containers/buildah v1.25.2-0.20220406205807-5b8e79118057/go.mod h1:iSoopbYRb6K4b5c3hXgXNkGTI/T085t2+XiGjceud94=
github.com/containers/common v0.47.5-0.20220331143923-5f14ec785c18/go.mod h1:Vr2Fn6EdzD6JNAbz8L8bTv3uWLv2p31Ih2O3EAK6Hyc=
github.com/containers/common v0.47.5-0.20220421111103-112a47964ddb h1:TBrx1KcmWcesByqTb4Cq7F6bg7bDOjqCf6+6rbi8x4k=
github.com/containers/common v0.47.5-0.20220421111103-112a47964ddb/go.mod h1:r80nWTmJrG9EoLkuI6WfbWQDUNQVqkVuB8Oaj1VVjOA=
github.com/containers/common v0.47.5-0.20220425182415-4081e6be9356 h1:eJ1ghvyswTLRywF4YYEWrzZyOFEzlD1FUPLzJSz+wKo=
github.com/containers/common v0.47.5-0.20220425182415-4081e6be9356/go.mod h1:r80nWTmJrG9EoLkuI6WfbWQDUNQVqkVuB8Oaj1VVjOA=
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I=
github.com/containers/image/v5 v5.19.2-0.20220224100137-1045fb70b094/go.mod h1:XoYK6kE0dpazFNcuS+a8lra+QfbC6s8tzv+cUuCrZpE=
Expand Down
3 changes: 2 additions & 1 deletion libpod/networking_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/containers/common/libnetwork/etchosts"
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/machine"
"github.com/containers/common/pkg/netns"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/libpod/events"
Expand Down Expand Up @@ -62,7 +63,7 @@ const (
// This is need because a HostIP of 127.0.0.1 would now allow the gvproxy forwarder to reach to open ports.
// For machine the HostIP must only be used by gvproxy and never in the VM.
func (c *Container) convertPortMappings() []types.PortMapping {
if !c.runtime.config.Engine.MachineEnabled || len(c.config.PortMappings) == 0 {
if !machine.IsGvProxyBased() || len(c.config.PortMappings) == 0 {
return c.config.PortMappings
}
// if we run in a machine VM we have to ignore the host IP part
Expand Down
5 changes: 3 additions & 2 deletions libpod/networking_machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/machine"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -117,15 +118,15 @@ func annotateGvproxyResponseError(r io.Reader) error {

// exposeMachinePorts exposes the ports for podman machine via gvproxy
func (r *Runtime) exposeMachinePorts(ports []types.PortMapping) error {
if !r.config.Engine.MachineEnabled {
if !machine.IsGvProxyBased() {
return nil
}
return requestMachinePorts(true, ports)
}

// unexposeMachinePorts closes the ports for podman machine via gvproxy
func (r *Runtime) unexposeMachinePorts(ports []types.PortMapping) error {
if !r.config.Engine.MachineEnabled {
if !machine.IsGvProxyBased() {
return nil
}
return requestMachinePorts(false, ports)
Expand Down
20 changes: 19 additions & 1 deletion pkg/machine/ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ ExecStart=/usr/bin/sleep infinity
containers := `[containers]
netns="bridge"
`
// Set deprecated machine_enabled until podman package on fcos is
// current enough to no longer require it
rootContainers := `[engine]
machine_enabled=true
`
Expand Down Expand Up @@ -392,7 +394,7 @@ Delegate=memory pids cpu io
FileEmbedded1: FileEmbedded1{Mode: intToPtr(0644)},
})

// Set machine_enabled to true to indicate we're in a VM
// Set deprecated machine_enabled to true to indicate we're in a VM
files = append(files, File{
Node: Node{
Group: getNodeGrp("root"),
Expand All @@ -408,6 +410,22 @@ Delegate=memory pids cpu io
},
})

// Set machine marker file to indicate podman is in a qemu based machine
files = append(files, File{
Node: Node{
Group: getNodeGrp("root"),
Path: "/etc/containers/podman-machine",
User: getNodeUsr("root"),
},
FileEmbedded1: FileEmbedded1{
Append: nil,
Contents: Resource{
Source: encodeDataURLPtr("qemu\n"),
},
Mode: intToPtr(0644),
},
})

// Issue #11489: make sure that we can inject a custom registries.conf
// file on the system level to force a single search registry.
// The remote client does not yet support prompting for short-name
Expand Down
4 changes: 4 additions & 0 deletions pkg/machine/wsl/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ func configureSystem(v *MachineVM, dist string) error {
return errors.Wrap(err, "could not create containers.conf for guest OS")
}

if err := runCmdPassThrough("wsl", "-d", dist, "sh", "-c", "echo wsl > /etc/containers/podman-machine"); err != nil {
return errors.Wrap(err, "could not create podman-machine file for guest OS")
}

return nil
}

Expand Down
27 changes: 25 additions & 2 deletions pkg/specgen/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na
err error
)

splitVol := strings.Split(vol, ":")
splitVol := SplitVolumeString(vol)
if len(splitVol) > 3 {
return nil, nil, nil, errors.Wrapf(volumeFormatErr, vol)
}
Expand Down Expand Up @@ -93,7 +93,7 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na
}
}

if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") {
if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") || isHostWinPath(src) {
// This is not a named volume
overlayFlag := false
chownFlag := false
Expand Down Expand Up @@ -152,3 +152,26 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na

return mounts, volumes, overlayVolumes, nil
}

// Splits a volume string, accounting for Win drive paths
// when running as a WSL linux guest or Windows client
func SplitVolumeString(vol string) []string {
parts := strings.Split(vol, ":")
if !shouldResolveWinPaths() {
return parts
}

// Skip extended marker prefix if present
n := 0
if strings.HasPrefix(vol, `\\?\`) {
n1hility marked this conversation as resolved.
Show resolved Hide resolved
n = 4
}

if hasWinDriveScheme(vol, n) {
first := parts[0] + ":" + parts[1]
parts = parts[1:]
parts[0] = first
}

return parts
}
59 changes: 59 additions & 0 deletions pkg/specgen/winpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package specgen

import (
"fmt"
"strings"
"unicode"

"github.com/pkg/errors"
)

func isHostWinPath(path string) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possible to get unittests on these functions to avoid simple regressions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baude these are tested via specgenutil_test which I originally had to do for API access reasons. Although, I think those reasons may no longer apply. I can see if I can move them to specgen

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baude ah yes, it creates an import cycle that was the other reason they had to be in the other package.

return shouldResolveWinPaths() && strings.HasPrefix(path, `\\`) || hasWinDriveScheme(path, 0) || winPathExists(path)
}

func hasWinDriveScheme(path string, start int) bool {
if len(path) < start+2 || path[start+1] != ':' {
return false
}

drive := rune(path[start])
return drive < unicode.MaxASCII && unicode.IsLetter(drive)
}

// Converts a Windows path to a WSL guest path if local env is a WSL linux guest or this is a Windows client.
func ConvertWinMountPath(path string) (string, error) {
if !shouldResolveWinPaths() {
return path, nil
}

if strings.HasPrefix(path, "/") {
// Handle /[driveletter]/windows/path form (e.g. c:\Users\bar == /c/Users/bar)
if len(path) > 2 && path[2] == '/' && shouldResolveUnixWinVariant(path) {
drive := unicode.ToLower(rune(path[1]))
if unicode.IsLetter(drive) && drive <= unicode.MaxASCII {
return fmt.Sprintf("/mnt/%c/%s", drive, path[3:]), nil
}
}

// unix path - pass through
return path, nil
}

// Convert remote win client relative paths to absolute
path = resolveRelativeOnWindows(path)

// Strip extended marker prefix if present
path = strings.TrimPrefix(path, `\\?\`)

// Drive installed via wsl --mount
if strings.HasPrefix(path, `\\.\`) {
path = "/mnt/wsl/" + path[4:]
} else if len(path) > 1 && path[1] == ':' {
path = "/mnt/" + strings.ToLower(path[0:1]) + path[2:]
} else {
return path, errors.New("unsupported UNC path")
}

return strings.ReplaceAll(path, `\`, "/"), nil
}
24 changes: 24 additions & 0 deletions pkg/specgen/winpath_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package specgen

import (
"os"

"github.com/containers/common/pkg/machine"
)

func shouldResolveWinPaths() bool {
return machine.MachineHostType() == "wsl"
}

func shouldResolveUnixWinVariant(path string) bool {
_, err := os.Stat(path)
return err != nil
}

func resolveRelativeOnWindows(path string) string {
return path
}

func winPathExists(path string) bool {
return false
}
20 changes: 20 additions & 0 deletions pkg/specgen/winpath_unsupported.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//go:build !linux && !windows
// +build !linux,!windows

package specgen

func shouldResolveWinPaths() bool {
return false
}

func shouldResolveUnixWinVariant(path string) bool {
return false
}

func resolveRelativeOnWindows(path string) string {
return path
}

func winPathExists(path string) bool {
return false
}
30 changes: 30 additions & 0 deletions pkg/specgen/winpath_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package specgen

import (
"github.com/sirupsen/logrus"
"os"
"path/filepath"
)

func shouldResolveUnixWinVariant(path string) bool {
return true
}

func shouldResolveWinPaths() bool {
return true
}

func resolveRelativeOnWindows(path string) string {
ret, err := filepath.Abs(path)
if err != nil {
logrus.Debugf("problem resolving possible relative path %q: %s", path, err.Error())
return path
}

return ret
}

func winPathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
77 changes: 77 additions & 0 deletions pkg/specgenutil/specgenutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//go:build linux
// +build linux

package specgenutil

import (
"testing"

"github.com/containers/common/pkg/machine"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/specgen"
"github.com/stretchr/testify/assert"
)

func TestWinPath(t *testing.T) {
const (
fail = false
pass = true
)
tests := []struct {
vol string
source string
dest string
isN bool
outcome bool
mach string
}{
{`C:\Foo:/blah`, "/mnt/c/Foo", "/blah", false, pass, "wsl"},
{`C:\Foo:/blah`, "/mnt/c/Foo", "/blah", false, fail, ""},
{`\\?\C:\Foo:/blah`, "/mnt/c/Foo", "/blah", false, pass, "wsl"},
{`/c/bar:/blah`, "/mnt/c/bar", "/blah", false, pass, "wsl"},
{`/c/bar:/blah`, "/c/bar", "/blah", false, pass, ""},
{`/test/this:/blah`, "/test/this", "/blah", false, pass, "wsl"},
{`c:/bar/something:/other`, "/mnt/c/bar/something", "/other", false, pass, "wsl"},
{`c:/foo:ro`, "c", "/foo", true, pass, ""},
{`\\computer\loc:/dest`, "", "", false, fail, "wsl"},
{`\\.\drive\loc:/target`, "/mnt/wsl/drive/loc", "/target", false, pass, "wsl"},
}

f := func(vol string, mach string) (*specgen.SpecGenerator, error) {
machine := machine.GetMachineMarker()
oldEnable, oldType := machine.Enabled, machine.Type
machine.Enabled, machine.Type = len(mach) > 0, mach
sg := specgen.NewSpecGenerator("nothing", false)
err := FillOutSpecGen(sg, &entities.ContainerCreateOptions{
ImageVolume: "ignore",
Volume: []string{vol}}, []string{},
)
machine.Enabled, machine.Type = oldEnable, oldType
return sg, err
}

for _, test := range tests {
msg := "Checking: " + test.vol
sg, err := f(test.vol, test.mach)
if test.outcome == fail {
assert.NotNil(t, err, msg)
continue
}
if !assert.Nil(t, err, msg) {
continue
}
if test.isN {
if !assert.Equal(t, 1, len(sg.Volumes), msg) {
continue
}
assert.Equal(t, test.source, sg.Volumes[0].Name, msg)
assert.Equal(t, test.dest, sg.Volumes[0].Dest, msg)
} else {
if !assert.Equal(t, 1, len(sg.Mounts), msg) {
continue
}
assert.Equal(t, test.source, sg.Mounts[0].Source, msg)
assert.Equal(t, test.dest, sg.Mounts[0].Destination, msg)
}
}
}
Loading