Skip to content

Commit 2c82e4f

Browse files
authored
Merge pull request #92 from rancher-sandbox/copy-command
Add copy/cp/scp command
2 parents d4fe56b + cfe3cc9 commit 2c82e4f

File tree

4 files changed

+111
-13
lines changed

4 files changed

+111
-13
lines changed

cmd/limactl/copy.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"os/user"
8+
"strings"
9+
10+
"github.com/AkihiroSuda/lima/pkg/sshutil"
11+
"github.com/AkihiroSuda/lima/pkg/store"
12+
"github.com/pkg/errors"
13+
"github.com/sirupsen/logrus"
14+
"github.com/urfave/cli/v2"
15+
)
16+
17+
var copyCommand = &cli.Command{
18+
Name: "copy",
19+
Aliases: []string{"cp"},
20+
Usage: "Copy files between host and guest",
21+
Description: "Prefix guest filenames with the instance name and a colon.\nExample: limactl copy default:/etc/os-release .",
22+
ArgsUsage: "SOURCE ... TARGET",
23+
Action: copyAction,
24+
}
25+
26+
func copyAction(clicontext *cli.Context) error {
27+
if clicontext.NArg() < 2 {
28+
return errors.Errorf("requires at least 2 arguments: SOURCE DEST")
29+
}
30+
arg0, err := exec.LookPath("scp")
31+
if err != nil {
32+
return err
33+
}
34+
u, err := user.Current()
35+
if err != nil {
36+
return err
37+
}
38+
39+
args, err := sshutil.CommonArgs()
40+
if err != nil {
41+
return err
42+
}
43+
args = append(args, "-3", "--")
44+
for _, arg := range clicontext.Args().Slice() {
45+
path := strings.Split(arg, ":")
46+
switch len(path) {
47+
case 1:
48+
args = append(args, arg)
49+
case 2:
50+
instName := path[0]
51+
inst, err := store.Inspect(instName)
52+
if err != nil {
53+
if errors.Is(err, os.ErrNotExist) {
54+
return errors.Errorf("instance %q does not exist, run `limactl start %s` to create a new instance", instName, instName)
55+
}
56+
return err
57+
}
58+
if inst.Status == store.StatusStopped {
59+
return errors.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
60+
}
61+
args = append(args, fmt.Sprintf("scp://%[email protected]:%d/%s", u.Username, inst.SSHLocalPort, path[1]))
62+
default:
63+
return errors.Errorf("Path %q contains multiple colons", arg)
64+
}
65+
}
66+
cmd := exec.Command(arg0, args...)
67+
cmd.Stdin = os.Stdin
68+
cmd.Stdout = os.Stdout
69+
cmd.Stderr = os.Stderr
70+
logrus.Debugf("executing scp (may take a long)): %+v", cmd.Args)
71+
72+
// TODO: use syscall.Exec directly (results in losing tty?)
73+
return cmd.Run()
74+
}

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func newApp() *cli.App {
4444
startCommand,
4545
stopCommand,
4646
shellCommand,
47+
copyCommand,
4748
listCommand,
4849
deleteCommand,
4950
validateCommand,

hack/test-example.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ limactl shell "$NAME" uname -a
7474
limactl shell "$NAME" cat /etc/os-release
7575
set +x
7676

77+
INFO "Testing limactl copy command"
78+
tmpfile="$HOME/lima-hostname"
79+
rm -f "$tmpfile"
80+
limactl cp "$NAME":/etc/hostname "$tmpfile"
81+
trap 'rm -f $tmpfile' EXIT
82+
expected="$(limactl shell "$NAME" cat /etc/hostname)"
83+
got="$(cat "$tmpfile")"
84+
INFO "/etc/hostname: expected=${expected}, got=${got}"
85+
if [ "$got" != "$expected" ]; then
86+
ERROR "copy command did not fetch the file"
87+
exit 1
88+
fi
89+
7790
if [[ -n ${CHECKS["systemd"]} ]]; then
7891
set -x
7992
if ! limactl shell "$NAME" systemctl is-system-running --wait; then

pkg/sshutil/sshutil.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,7 @@ func RemoveKnownHostEntries(sshLocalPort int) error {
106106
return nil
107107
}
108108

109-
func SSHArgs(instDir string) ([]string, error) {
110-
u, err := user.Current()
111-
if err != nil {
112-
return nil, err
113-
}
114-
controlSock := filepath.Join(instDir, filenames.SSHSock)
115-
if len(controlSock) >= osutil.UnixPathMax {
116-
return nil, errors.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax)
117-
}
109+
func CommonArgs() ([]string, error) {
118110
configDir, err := store.LimaConfigDir()
119111
if err != nil {
120112
return nil, err
@@ -149,10 +141,6 @@ func SSHArgs(instDir string) ([]string, error) {
149141
}
150142

151143
args = append(args,
152-
"-l", u.Username, // guest and host have the same username, but we should specify the username explicitly (#85)
153-
"-o", "ControlMaster=auto",
154-
"-o", "ControlPath=" + controlSock,
155-
"-o", "ControlPersist=5m",
156144
"-o", "StrictHostKeyChecking=no",
157145
"-o", "NoHostAuthenticationForLocalhost=yes",
158146
"-o", "GSSAPIAuthentication=no",
@@ -162,3 +150,25 @@ func SSHArgs(instDir string) ([]string, error) {
162150
)
163151
return args, nil
164152
}
153+
154+
func SSHArgs(instDir string) ([]string, error) {
155+
controlSock := filepath.Join(instDir, filenames.SSHSock)
156+
if len(controlSock) >= osutil.UnixPathMax {
157+
return nil, errors.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax)
158+
}
159+
u, err := user.Current()
160+
if err != nil {
161+
return nil, err
162+
}
163+
args, err := CommonArgs()
164+
if err != nil {
165+
return nil, err
166+
}
167+
args = append(args,
168+
"-l", u.Username, // guest and host have the same username, but we should specify the username explicitly (#85)
169+
"-o", "ControlMaster=auto",
170+
"-o", "ControlPath="+controlSock,
171+
"-o", "ControlPersist=5m",
172+
)
173+
return args, nil
174+
}

0 commit comments

Comments
 (0)