diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index a285d7d12c1..bf7621059bb 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -13,6 +13,7 @@ import ( "github.com/containers/podman/v6/cmd/podman/common" "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/cmd/podman/utils" + "github.com/containers/podman/v6/internal/localapi" "github.com/containers/podman/v6/libpod/define" "github.com/containers/podman/v6/pkg/domain/entities" "github.com/containers/podman/v6/pkg/specgen" @@ -159,6 +160,7 @@ func create(cmd *cobra.Command, args []string) error { if err := specgenutil.FillOutSpecGen(s, &cliVals, args); err != nil { return err } + localapi.WarnIfMachineVolumesUnavailable(registry.PodmanConfig(), cliVals.Volume) s.RawImageName = rawImageName // Include the command used to create the container. diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index c13c65e4be6..e18f2e63923 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -9,6 +9,7 @@ import ( "github.com/containers/podman/v6/cmd/podman/common" "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/cmd/podman/utils" + "github.com/containers/podman/v6/internal/localapi" "github.com/containers/podman/v6/libpod/define" "github.com/containers/podman/v6/pkg/domain/entities" "github.com/containers/podman/v6/pkg/rootless" @@ -205,6 +206,7 @@ func run(cmd *cobra.Command, args []string) error { if err := specgenutil.FillOutSpecGen(s, &cliVals, args); err != nil { return err } + localapi.WarnIfMachineVolumesUnavailable(registry.PodmanConfig(), cliVals.Volume) s.RawImageName = rawImageName // Include the command used to create the container. diff --git a/internal/localapi/machine_volume_warning.go b/internal/localapi/machine_volume_warning.go new file mode 100644 index 00000000000..cef1c370331 --- /dev/null +++ b/internal/localapi/machine_volume_warning.go @@ -0,0 +1,116 @@ +//go:build amd64 || arm64 + +package localapi + +import ( + "net/url" + "path/filepath" + "sort" + "strings" + + "github.com/containers/podman/v6/pkg/domain/entities" + "github.com/containers/podman/v6/pkg/machine/define" + "github.com/containers/podman/v6/pkg/machine/vmconfigs" + "github.com/containers/podman/v6/pkg/specgen" + "github.com/sirupsen/logrus" +) + +const machineVolumesDocURL = "https://docs.podman.io/en/latest/markdown/podman-machine-init.1.html#volume" + +// WarnIfMachineVolumesUnavailable inspects bind mounts requested via --volume +// and warns if the source paths are not shared with the active Podman machine. +func WarnIfMachineVolumesUnavailable(cfg *entities.PodmanConfig, volumeSpecs []string) { + if cfg == nil || len(volumeSpecs) == 0 || !cfg.MachineMode { + return + } + + parsedURI, err := url.Parse(cfg.URI) + if err != nil { + logrus.Debugf("skipping machine volume check, invalid connection URI %q: %v", cfg.URI, err) + return + } + + mounts, vmType, err := getMachineMountsAndVMType(cfg.URI, parsedURI) + if err != nil { + logrus.Debugf("skipping machine volume check: %v", err) + return + } + if vmType == define.WSLVirt { + // WSL mounts the drives automatically so a warning would be misleading. + return + } + + missing := collectUnsharedHostPaths(volumeSpecs, mounts, vmType) + if len(missing) == 0 { + return + } + sort.Strings(missing) + logrus.Warnf("The following bind mount sources are not shared with the Podman machine and may not work: %s. See %s for details on configuring machine volumes.", strings.Join(missing, ", "), machineVolumesDocURL) +} + +func collectUnsharedHostPaths(volumeSpecs []string, mounts []*vmconfigs.Mount, vmType define.VMType) []string { + unshared := []string{} + seen := make(map[string]struct{}) + for _, spec := range volumeSpecs { + src, ok := extractBindMountSource(spec) + if !ok { + continue + } + if _, found := isPathAvailableOnMachine(mounts, vmType, src); found { + continue + } + normalized, err := normalizeVolumeSource(src) + if err != nil { + logrus.Debugf("machine volume check: unable to normalize %q: %v", src, err) + continue + } + if _, exists := seen[normalized]; !exists { + unshared = append(unshared, normalized) + seen[normalized] = struct{}{} + } + } + return unshared +} + +func extractBindMountSource(spec string) (string, bool) { + parts := specgen.SplitVolumeString(spec) + if len(parts) <= 1 { + return "", false + } + src := parts[0] + if len(src) == 0 { + return "", false + } + if strings.HasPrefix(src, "./") { + resolved, err := filepath.EvalSymlinks(src) + if err != nil { + logrus.Debugf("machine volume check: failed to resolve symlinks of %q: %v", src, err) + } else { + path, err := filepath.Abs(resolved) + if err != nil { + logrus.Debugf("machine volume check: failed to get absolute path of %q: %v", resolved, err) + } else { + src = path + } + } + } + + if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") || specgen.IsHostWinPath(src) { + return src, true + } + return "", false +} + +func normalizeVolumeSource(path string) (string, error) { + if specgen.IsHostWinPath(path) { + return filepath.Clean(path), nil + } + if filepath.IsAbs(path) { + return filepath.Clean(path), nil + } + absPath, err := filepath.Abs(path) + if err != nil { + return "", err + } + return absPath, nil +} diff --git a/internal/localapi/machine_volume_warning_stub.go b/internal/localapi/machine_volume_warning_stub.go new file mode 100644 index 00000000000..0a4b9ff7e4f --- /dev/null +++ b/internal/localapi/machine_volume_warning_stub.go @@ -0,0 +1,12 @@ +//go:build !amd64 && !arm64 + +package localapi + +import ( + "github.com/containers/podman/v6/pkg/domain/entities" + "github.com/sirupsen/logrus" +) + +func WarnIfMachineVolumesUnavailable(_ *entities.PodmanConfig, _ []string) { + logrus.Debug("skipping machine volume check: podman machine mode not supported") +} diff --git a/internal/localapi/machine_volume_warning_test.go b/internal/localapi/machine_volume_warning_test.go new file mode 100644 index 00000000000..43f3c1748b0 --- /dev/null +++ b/internal/localapi/machine_volume_warning_test.go @@ -0,0 +1,44 @@ +//go:build amd64 || arm64 + +package localapi + +import ( + "os" + "path/filepath" + "testing" + + "github.com/containers/podman/v6/pkg/machine/define" + "github.com/containers/podman/v6/pkg/machine/vmconfigs" +) + +func TestCollectUnsharedHostPaths(t *testing.T) { + t.Parallel() + + tmp := t.TempDir() + shared := filepath.Join(tmp, "shared") + nested := filepath.Join(shared, "nested") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("mkdir nested: %v", err) + } + unshared := filepath.Join(tmp, "unshared") + + mounts := []*vmconfigs.Mount{ + {Source: shared}, + } + + volumes := []string{ + shared + ":/data", + nested + ":/nested", + unshared + ":/fail", + unshared + ":/fail2", // duplicate should only be reported once + "namedVolume:/ctr", + } + + missing := collectUnsharedHostPaths(volumes, mounts, define.QemuVirt) + if len(missing) != 1 { + t.Fatalf("expected 1 missing mount, got %d (%v)", len(missing), missing) + } + if filepath.Clean(missing[0]) != filepath.Clean(unshared) { + t.Fatalf("expected missing path %q, got %q", unshared, missing[0]) + } +}