diff --git a/cmd/podman/volumes/exists.go b/cmd/podman/volumes/exists.go index 3cac2722072f..b1bdf9da5a3d 100644 --- a/cmd/podman/volumes/exists.go +++ b/cmd/podman/volumes/exists.go @@ -10,7 +10,7 @@ var ( volumeExistsDescription = `If the given volume exists, podman volume exists exits with 0, otherwise the exit code will be 1.` volumeExistsCommand = &cobra.Command{ Use: "exists VOLUME", - Short: "volume exists", + Short: "Volume exists", Long: volumeExistsDescription, RunE: volumeExists, Example: `podman volume exists myvol`, diff --git a/cmd/podman/volumes/mount.go b/cmd/podman/volumes/mount.go new file mode 100644 index 000000000000..1f78187b8f0f --- /dev/null +++ b/cmd/podman/volumes/mount.go @@ -0,0 +1,51 @@ +package volumes + +import ( + "fmt" + + "github.com/containers/podman/v4/cmd/podman/common" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/utils" + "github.com/spf13/cobra" +) + +var ( + volumeMountDescription = `Mount a volume and return the mountpoint` + volumeMountCommand = &cobra.Command{ + Annotations: map[string]string{ + registry.UnshareNSRequired: "", + registry.ParentNSRequired: "", + registry.EngineMode: registry.ABIMode, + }, + Use: "mount NAME", + Short: "Mount volume", + Long: volumeMountDescription, + RunE: volumeMount, + Example: `podman volume mount myvol`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.AutocompleteVolumes, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: volumeMountCommand, + Parent: volumeCmd, + }) +} + +func volumeMount(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + reports, err := registry.ContainerEngine().VolumeMount(registry.GetContext(), args) + if err != nil { + return err + } + for _, r := range reports { + if r.Err == nil { + fmt.Println(r.Path) + continue + } + errs = append(errs, r.Err) + } + return errs.PrintErrors() +} diff --git a/cmd/podman/volumes/unmount.go b/cmd/podman/volumes/unmount.go new file mode 100644 index 000000000000..dd0cebc06829 --- /dev/null +++ b/cmd/podman/volumes/unmount.go @@ -0,0 +1,48 @@ +package volumes + +import ( + "fmt" + + "github.com/containers/podman/v4/cmd/podman/common" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/utils" + "github.com/spf13/cobra" +) + +var ( + volumeUnmountDescription = `Unmount a volume` + volumeUnmountCommand = &cobra.Command{ + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + Use: "unmount NAME", + Short: "Unmount volume", + Long: volumeUnmountDescription, + RunE: volumeUnmount, + Example: `podman volume unmount myvol`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.AutocompleteVolumes, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: volumeUnmountCommand, + Parent: volumeCmd, + }) +} + +func volumeUnmount(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + reports, err := registry.ContainerEngine().VolumeUnmount(registry.GetContext(), args) + if err != nil { + return err + } + for _, r := range reports { + var errs utils.OutputErrors + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/docs/source/markdown/podman-volume-mount.1.md b/docs/source/markdown/podman-volume-mount.1.md new file mode 100644 index 000000000000..a5f35a34d711 --- /dev/null +++ b/docs/source/markdown/podman-volume-mount.1.md @@ -0,0 +1,28 @@ +% podman-volume-mount(1) + +## NAME +podman\-volume\-mount - Mount a volume filesystem + +## SYNOPSIS +**podman volume mount** [*volume* ...] + +## DESCRIPTION +Mounts the specified volumes' file system in a location which can be +accessed from the host, and returns its location. + +Rootless mode only supports mounting file volumes, unless you enter the user namespace +via the `podman unshare` command. All other volume types will fail to mount. + +## RETURN VALUE +The location of the mounted file system. On error an empty string and errno is +returned. + +## EXAMPLE + +``` +podman volume mount foo +/home/dwalsh/.local/share/containers/storage/volumes/foo/_data +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-unmount(1)](podman-volume-unmount.1.md)**, **[podman-unshare(1)](podman-unshare.1.md)**, **mount(8)** diff --git a/docs/source/markdown/podman-volume-unmount.1.md b/docs/source/markdown/podman-volume-unmount.1.md new file mode 100644 index 000000000000..e2fcd425f074 --- /dev/null +++ b/docs/source/markdown/podman-volume-unmount.1.md @@ -0,0 +1,27 @@ +% podman-volume-unmount(1) + +## NAME +podman\-volume\-unmount - Unmount a volume + +## SYNOPSIS +**podman volume unmount** *volume* [...] + +**podman volume umount** *volume* [...] + +## DESCRIPTION +Unmounts the specified volume, if there are no other containers +using it. + +Volume storage increments a mount counter each time a volume is mounted. +When a volume is unmounted, the mount counter is decremented, and the +volume's filesystem is physically unmounted only when the mount +counter reaches zero indicating no other processes are using the mount. + +## EXAMPLE + +podman volume unmount volumeID + +podman volume unmount volumeID1 volumeID2 volumeID3 + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-mount(1)](podman-volume-mount.1.md)** diff --git a/docs/source/markdown/podman-volume.1.md b/docs/source/markdown/podman-volume.1.md index d8bc11aad7e1..d05f007c83c2 100644 --- a/docs/source/markdown/podman-volume.1.md +++ b/docs/source/markdown/podman-volume.1.md @@ -19,8 +19,10 @@ podman volume is a set of subcommands that manage volumes. | import | [podman-volume-import(1)](podman-volume-import.1.md) | Import tarball contents into a podman volume. | | inspect | [podman-volume-inspect(1)](podman-volume-inspect.1.md) | Get detailed information on one or more volumes. | | ls | [podman-volume-ls(1)](podman-volume-ls.1.md) | List all the available volumes. | +| mount | [podman-volume-mount(1)](podman-volume-mount.1.md) | Mount a volume filesystem. | | prune | [podman-volume-prune(1)](podman-volume-prune.1.md) | Remove all unused volumes. | | rm | [podman-volume-rm(1)](podman-volume-rm.1.md) | Remove one or more volumes. | +| unmount | [podman-volume-unmount(1)](podman-volume-unmount.1.md) | Unmount a volume. | ## SEE ALSO **[podman(1)](podman.1.md)** diff --git a/libpod/volume.go b/libpod/volume.go index d60d978ed22d..f79ceaa87b13 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -255,3 +255,16 @@ func (v *Volume) IsDangling() (bool, error) { func (v *Volume) UsesVolumeDriver() bool { return !(v.config.Driver == define.VolumeDriverLocal || v.config.Driver == "") } + +func (v *Volume) Mount() (string, error) { + v.lock.Lock() + defer v.lock.Unlock() + err := v.mount() + return v.config.MountPoint, err +} + +func (v *Volume) Unmount() error { + v.lock.Lock() + defer v.lock.Unlock() + return v.unmount(false) +} diff --git a/libpod/volume_internal_linux.go b/libpod/volume_internal_linux.go index 60d3667a99ce..7d7dea9d0849 100644 --- a/libpod/volume_internal_linux.go +++ b/libpod/volume_internal_linux.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package libpod diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 21272d33ffe9..7cf7ca17fbf4 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -99,6 +99,8 @@ type ContainerEngine interface { VolumeMounted(ctx context.Context, namesOrID string) (*BoolReport, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts InspectOptions) ([]*VolumeInspectReport, []error, error) VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) + VolumeMount(ctx context.Context, namesOrIds []string) ([]*VolumeMountReport, error) VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*reports.PruneReport, error) VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) + VolumeUnmount(ctx context.Context, namesOrIds []string) ([]*VolumeUnmountReport, error) } diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index 9bdce8392873..f2e60a0db28a 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -187,3 +187,17 @@ type VolumeCreateBody struct { // Required: true Name string `json:"Name"` } + +// VolumeMountReport describes the response from volume mount +type VolumeMountReport struct { + Err error + Id string //nolint + Name string + Path string +} + +// VolumeUnmountReport describes the response from umounting a volume +type VolumeUnmountReport struct { + Err error + Id string //nolint +} diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index ee7269807c56..19fc6d2d3d77 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -178,3 +178,35 @@ func (ic *ContainerEngine) VolumeMounted(ctx context.Context, nameOrID string) ( } return &entities.BoolReport{Value: false}, nil } + +func (ic *ContainerEngine) VolumeMount(ctx context.Context, nameOrIDs []string) ([]*entities.VolumeMountReport, error) { + reports := []*entities.VolumeMountReport{} + for _, name := range nameOrIDs { + report := entities.VolumeMountReport{Id: name} + vol, err := ic.Libpod.LookupVolume(name) + if err != nil { + report.Err = err + } else { + report.Path, report.Err = vol.Mount() + } + reports = append(reports, &report) + } + + return reports, nil +} + +func (ic *ContainerEngine) VolumeUnmount(ctx context.Context, nameOrIDs []string) ([]*entities.VolumeUnmountReport, error) { + reports := []*entities.VolumeUnmountReport{} + for _, name := range nameOrIDs { + report := entities.VolumeUnmountReport{Id: name} + vol, err := ic.Libpod.LookupVolume(name) + if err != nil { + report.Err = err + } else { + report.Err = vol.Unmount() + } + reports = append(reports, &report) + } + + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index f4abeab0fca8..33e090148167 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -100,3 +100,11 @@ func (ic *ContainerEngine) VolumeExists(ctx context.Context, nameOrID string) (* func (ic *ContainerEngine) VolumeMounted(ctx context.Context, nameOrID string) (*entities.BoolReport, error) { return nil, errors.New("not implemented") } + +func (ic *ContainerEngine) VolumeMount(ctx context.Context, nameOrIDs []string) ([]*entities.VolumeMountReport, error) { + return nil, errors.New("mounting volumes is not supported for remote clients") +} + +func (ic *ContainerEngine) VolumeUnmount(ctx context.Context, nameOrIDs []string) ([]*entities.VolumeUnmountReport, error) { + return nil, errors.New("unmounting volumes is not supported for remote clients") +} diff --git a/test/system/160-volumes.bats b/test/system/160-volumes.bats index a3c972b3e0a0..d0088b9941a9 100644 --- a/test/system/160-volumes.bats +++ b/test/system/160-volumes.bats @@ -387,4 +387,29 @@ NeedsChown | true run_podman volume rm $myvolume } +@test "podman volume mount" { + skip_if_remote "podman --remote volume mount not supported" + myvolume=myvol$(random_string) + myfile=myfile$(random_string) + mytext=$(random_string) + + # Create a named volume + run_podman volume create $myvolume + is "$output" "$myvolume" "output from volume create" + + if ! is_rootless ; then + # image mount is hard to test as a rootless user + # and does not work remotely + run_podman volume mount ${myvolume} + mnt=${output} + echo $mytext >$mnt/$myfile + run_podman run -v ${myvolume}:/vol:z $IMAGE cat /vol/$myfile + is "$output" "$mytext" "$myfile should exist within the containers volume and contain $mytext" + run_podman volume unmount ${myvolume} + else + run_podman 125 volume mount ${myvolume} + is "$output" "Error: cannot run command \"podman volume mount\" in rootless mode, must execute.*podman unshare.*first" "Should fail and complain about unshare" + fi +} + # vim: filetype=sh