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

Add podman machine cp subcommand #25331

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
158 changes: 158 additions & 0 deletions cmd/podman/machine/cp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//go:build amd64 || arm64

package machine

import (
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"

"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/libpod/events"
"github.com/containers/podman/v5/pkg/copy"
"github.com/containers/podman/v5/pkg/machine"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/env"
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/spf13/cobra"
)

type cpOptions struct {
Quiet bool
Machine *vmconfigs.MachineConfig
IsSrc bool
SrcPath string
DestPath string
}

var (
cpCmd = &cobra.Command{
Use: "cp [options] SRC_PATH DEST_PATH",
Short: "Securely copy contents between the virtual machine",
Long: "Securely copy files or directories between the virtual machine and your host",
PersistentPreRunE: machinePreRunE,
RunE: cp,
Args: cobra.ExactArgs(2),
Example: `podman machine cp ~/ca.crt podman-machine-default:/etc/containers/certs.d/ca.crt`,
ValidArgsFunction: autocompleteMachineCp,
}

cpOpts = cpOptions{}
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: cpCmd,
Parent: machineCmd,
})

flags := cpCmd.Flags()
quietFlagName := "quiet"
flags.BoolVarP(&cpOpts.Quiet, quietFlagName, "q", false, "Suppress copy status output")
}

func cp(_ *cobra.Command, args []string) error {
var err error

srcMachine, srcPath, destMachine, destPath, err := copy.ParseSourceAndDestination(args[0], args[1])
if err != nil {
return err
}

// Passing an absolute windows path of the format <volume>:\<path> will cause
// `copy.ParseSourceAndDestination` to think the volume is a Machine. Check
Copy link
Member

Choose a reason for hiding this comment

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

just thinking about it does podman cp not have the exact same issue, i.e. should this fix be moved into ParseSourceAndDestination()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Wouldn't that result in the same issue mentioned below having to do with single name containers?

// if the raw cmdline argument is a Windows host path.
if runtime.GOOS == "windows" {
if specgen.IsHostWinPath(args[0]) {
srcMachine = ""
srcPath = args[0]
}

if specgen.IsHostWinPath(args[1]) {
destMachine = ""
destPath = args[1]
}
Comment on lines +70 to +78
Copy link
Member

Choose a reason for hiding this comment

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

Also isn't this breaking for machine names with one letter? i.e. I think it runs into the same issue as shown in #25323

We do allow a machine called C so that can be a problem although super unlikely that anyone would be using single letter machine names hopefully so I am fine to ignore that part for now. Though adding this issue in a comment here might help future readers.

}

mc, err := resolveMachine(srcMachine, destMachine)
if err != nil {
return err
}

state, err := provider.State(mc, false)
if err != nil {
return err
}
if state != define.Running {
return fmt.Errorf("vm %q is not running", mc.Name)
}

cpOpts.Machine = mc
cpOpts.SrcPath = srcPath
cpOpts.DestPath = destPath

err = secureCopy(&cpOpts)
if err != nil {
return fmt.Errorf("copy failed: %s", err.Error())
}

fmt.Println("Copy successful")
newMachineEvent(events.Copy, events.Event{Name: mc.Name})
return nil
}

func secureCopy(opts *cpOptions) error {
srcPath := opts.SrcPath
destPath := opts.DestPath
sshConfig := opts.Machine.SSH

username := sshConfig.RemoteUsername
if cpOpts.Machine.HostUser.Rootful {
username = "root"
}
username += "@localhost:"

if opts.IsSrc {
srcPath = username + srcPath
} else {
destPath = username + destPath
}

args := []string{"-r", "-i", sshConfig.IdentityPath, "-P", strconv.Itoa(sshConfig.Port)}
args = append(args, machine.CommonSSHArgs()...)
args = append(args, []string{srcPath, destPath}...)

cmd := exec.Command("scp", args...)
if !opts.Quiet {
cmd.Stdout = os.Stdout
}
cmd.Stderr = os.Stderr
return cmd.Run()
}

func resolveMachine(srcMachine, destMachine string) (*vmconfigs.MachineConfig, error) {
if len(srcMachine) > 0 && len(destMachine) > 0 {
return nil, errors.New("copying between two machines is unsupported")
}

if len(srcMachine) == 0 && len(destMachine) == 0 {
return nil, errors.New("a machine name must prefix either the source path or destination path")
}

dirs, err := env.GetMachineDirs(provider.VMType())
if err != nil {
return nil, err
}

name := destMachine
if len(srcMachine) > 0 {
cpOpts.IsSrc = true
name = srcMachine
}

return vmconfigs.LoadMachineByName(name, dirs)
}
34 changes: 34 additions & 0 deletions cmd/podman/machine/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,40 @@ func autocompleteMachineSSH(cmd *cobra.Command, args []string, toComplete string
return nil, cobra.ShellCompDirectiveDefault
}

// autocompleteMachineCp - Autocomplete machine cp command.
func autocompleteMachineCp(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) < 2 {
if i := strings.IndexByte(toComplete, ':'); i > -1 {
// the user already set the machine name, so don't use the host file autocompletion
return nil, cobra.ShellCompDirectiveNoFileComp
}

suffixCompSlice := func(suf string, slice []string) []string {
for i := range slice {
key, val, hasVal := strings.Cut(slice[i], "\t")
if hasVal {
slice[i] = key + suf + "\t" + val
} else {
slice[i] += suf
}
}
return slice
}

// suggest machine when they match the input otherwise normal shell completion is used
machines, _ := getMachines(toComplete)
for _, machine := range machines {
if strings.HasPrefix(machine, toComplete) {
return suffixCompSlice(":", machines), cobra.ShellCompDirectiveNoSpace
}
}

return nil, cobra.ShellCompDirectiveNoSpace
}
// don't complete more than 2 args
return nil, cobra.ShellCompDirectiveNoFileComp
}

// autocompleteMachine - Autocomplete machines.
func autocompleteMachine(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
Expand Down
63 changes: 63 additions & 0 deletions docs/source/markdown/podman-machine-cp.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
% podman-machine-cp 1

## NAME
podman\-machine\-cp - Securely copy contents between the host and the virtual machine

## SYNOPSIS
**podman machine cp** [*options*] *src_path* *dest_path*

## DESCRIPTION

Use secure copy (scp) to copy files or directories between the virtual machine
and your host machine.

`podman machine cp` does not support copying between two virtual machines,
which would require two machines running simultaneously.

Additionally, `podman machine cp` will automatically do a recursive copy of
files and directories.

## OPTIONS

#### **--help**

Print usage statement.

#### **--quiet**, **-q**

Suppress copy status output.

## EXAMPLES
Copy a file from your host to the running Podman Machine.
```
$ podman machine cp ~/configuration.txt podman-machine-default:~/configuration.txt
...
Copy Successful
```

Copy a file from the running Podman Machine to your host.
```
$ podman machine cp podman-machine-default:~/logs/log.txt ~/logs/podman-machine-default.txt
...
Copy Successful
```

Copy a directory from your host to the running Podman Machine.
```
$ podman machine cp ~/.config podman-machine-default:~/.config
...
Copy Successful
```

Copy a directory from the running Podman Machine to your host.
```
$ podman machine cp podman-machine-default:~/.config ~/podman-machine-default.config
...
Copy Successful
```

## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**

## HISTORY
February 2025, Originally compiled by Jake Correnti <[email protected]>
29 changes: 15 additions & 14 deletions docs/source/markdown/podman-machine.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,23 @@ Podman machine behaviour can be modified via the [machine] section in the contai

## SUBCOMMANDS

| Command | Man Page | Description |
|---------|----------------------------------------------------------|---------------------------------------|
| info | [podman-machine-info(1)](podman-machine-info.1.md) | Display machine host info |
| init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine |
| inspect | [podman-machine-inspect(1)](podman-machine-inspect.1.md) | Inspect one or more virtual machines |
| list | [podman-machine-list(1)](podman-machine-list.1.md) | List virtual machines |
| os | [podman-machine-os(1)](podman-machine-os.1.md) | Manage a Podman virtual machine's OS |
| reset | [podman-machine-reset(1)](podman-machine-reset.1.md) | Reset Podman machines and environment |
| rm | [podman-machine-rm(1)](podman-machine-rm.1.md) | Remove a virtual machine |
| set | [podman-machine-set(1)](podman-machine-set.1.md) | Set a virtual machine setting |
| ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine |
| start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine |
| stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine |
| Command | Man Page | Description |
|---------|----------------------------------------------------------|-----------------------------------------------------------------|
| cp | [podman-machine-cp(1)](podman-machine-cp.1.md) | Securely copy contents between the host and the virtual machine |
| info | [podman-machine-info(1)](podman-machine-info.1.md) | Display machine host info |
| init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine |
| inspect | [podman-machine-inspect(1)](podman-machine-inspect.1.md) | Inspect one or more virtual machines |
| list | [podman-machine-list(1)](podman-machine-list.1.md) | List virtual machines |
| os | [podman-machine-os(1)](podman-machine-os.1.md) | Manage a Podman virtual machine's OS |
| reset | [podman-machine-reset(1)](podman-machine-reset.1.md) | Reset Podman machines and environment |
| rm | [podman-machine-rm(1)](podman-machine-rm.1.md) | Remove a virtual machine |
| set | [podman-machine-set(1)](podman-machine-set.1.md) | Set a virtual machine setting |
| ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine |
| start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine |
| stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine |

## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-machine-info(1)](podman-machine-info.1.md)**, **[podman-machine-init(1)](podman-machine-init.1.md)**, **[podman-machine-list(1)](podman-machine-list.1.md)**, **[podman-machine-os(1)](podman-machine-os.1.md)**, **[podman-machine-rm(1)](podman-machine-rm.1.md)**, **[podman-machine-ssh(1)](podman-machine-ssh.1.md)**, **[podman-machine-start(1)](podman-machine-start.1.md)**, **[podman-machine-stop(1)](podman-machine-stop.1.md)**, **[podman-machine-inspect(1)](podman-machine-inspect.1.md)**, **[podman-machine-reset(1)](podman-machine-reset.1.md)**, **containers.conf(5)**
**[podman(1)](podman.1.md)**, **[podman-machine-cp(1)](podman-machine-cp.1.md)**, **[podman-machine-info(1)](podman-machine-info.1.md)**, **[podman-machine-init(1)](podman-machine-init.1.md)**, **[podman-machine-list(1)](podman-machine-list.1.md)**, **[podman-machine-os(1)](podman-machine-os.1.md)**, **[podman-machine-rm(1)](podman-machine-rm.1.md)**, **[podman-machine-ssh(1)](podman-machine-ssh.1.md)**, **[podman-machine-start(1)](podman-machine-start.1.md)**, **[podman-machine-stop(1)](podman-machine-stop.1.md)**, **[podman-machine-inspect(1)](podman-machine-inspect.1.md)**, **[podman-machine-reset(1)](podman-machine-reset.1.md)**, **containers.conf(5)**

### Troubleshooting

Expand Down
37 changes: 37 additions & 0 deletions pkg/machine/e2e/config_cp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package e2e_test

type cpMachine struct {
quiet bool
src string
dest string

cmd []string
}

func (c *cpMachine) buildCmd(m *machineTestBuilder) []string {
cmd := []string{"machine", "cp"}

if c.quiet {
cmd = append(cmd, "--quiet")
}

cmd = append(cmd, c.src, c.dest)

c.cmd = cmd
return cmd
}

func (c *cpMachine) withQuiet() *cpMachine {
c.quiet = true
return c
}

func (c *cpMachine) withSrc(src string) *cpMachine {
c.src = src
return c
}

func (c *cpMachine) withDest(dest string) *cpMachine {
c.dest = dest
return c
}
Loading