Skip to content

Commit 7c55a22

Browse files
committed
add rsync flag option to copy files using rsync
Signed-off-by: olalekan odukoya <[email protected]>
1 parent ccd3c0c commit 7c55a22

File tree

3 files changed

+168
-22
lines changed

3 files changed

+168
-22
lines changed

cmd/limactl/copy.go

+127-21
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ Prefix guest filenames with the instance name and a colon.
2121
Example: limactl copy default:/etc/os-release .
2222
`
2323

24+
type copyTool string
25+
26+
const (
27+
rsync copyTool = "rsync"
28+
scp copyTool = "scp"
29+
)
30+
2431
func newCopyCommand() *cobra.Command {
2532
copyCommand := &cobra.Command{
2633
Use: "copy SOURCE ... TARGET",
@@ -49,13 +56,6 @@ func copyAction(cmd *cobra.Command, args []string) error {
4956
return err
5057
}
5158

52-
arg0, err := exec.LookPath("scp")
53-
if err != nil {
54-
return err
55-
}
56-
instances := make(map[string]*store.Instance)
57-
scpFlags := []string{}
58-
scpArgs := []string{}
5959
debug, err := cmd.Flags().GetBool("debug")
6060
if err != nil {
6161
return err
@@ -65,6 +65,45 @@ func copyAction(cmd *cobra.Command, args []string) error {
6565
verbose = true
6666
}
6767

68+
cpTool := rsync
69+
arg0, err := exec.LookPath(string(cpTool))
70+
if err != nil {
71+
arg0, err = exec.LookPath(string(cpTool))
72+
if err != nil {
73+
return err
74+
}
75+
}
76+
logrus.Infof("using copy tool %q", arg0)
77+
78+
var copyCmd *exec.Cmd
79+
switch cpTool {
80+
case scp:
81+
copyCmd, err = scpCommand(arg0, args, verbose, recursive)
82+
case rsync:
83+
copyCmd, err = rsyncCommand(arg0, args, verbose, recursive)
84+
default:
85+
err = fmt.Errorf("invalid copy tool %q", cpTool)
86+
}
87+
if err != nil {
88+
return err
89+
}
90+
91+
copyCmd.Stdin = cmd.InOrStdin()
92+
copyCmd.Stdout = cmd.OutOrStdout()
93+
copyCmd.Stderr = cmd.ErrOrStderr()
94+
logrus.Debugf("executing %v (may take a long time)", copyCmd)
95+
96+
// TODO: use syscall.Exec directly (results in losing tty?)
97+
return copyCmd.Run()
98+
}
99+
100+
func scpCommand(command string, args []string, verbose, recursive bool) (*exec.Cmd, error) {
101+
instances := make(map[string]*store.Instance)
102+
103+
scpFlags := []string{}
104+
scpArgs := []string{}
105+
var err error
106+
68107
if verbose {
69108
scpFlags = append(scpFlags, "-v")
70109
} else {
@@ -74,6 +113,7 @@ func copyAction(cmd *cobra.Command, args []string) error {
74113
if recursive {
75114
scpFlags = append(scpFlags, "-r")
76115
}
116+
77117
// this assumes that ssh and scp come from the same place, but scp has no -V
78118
legacySSH := sshutil.DetectOpenSSHVersion("ssh").LessThan(*semver.New("8.0.0"))
79119
for _, arg := range args {
@@ -86,12 +126,12 @@ func copyAction(cmd *cobra.Command, args []string) error {
86126
inst, err := store.Inspect(instName)
87127
if err != nil {
88128
if errors.Is(err, os.ErrNotExist) {
89-
return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
129+
return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
90130
}
91-
return err
131+
return nil, err
92132
}
93133
if inst.Status == store.StatusStopped {
94-
return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
134+
return nil, fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
95135
}
96136
if legacySSH {
97137
scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort))
@@ -101,11 +141,11 @@ func copyAction(cmd *cobra.Command, args []string) error {
101141
}
102142
instances[instName] = inst
103143
default:
104-
return fmt.Errorf("path %q contains multiple colons", arg)
144+
return nil, fmt.Errorf("path %q contains multiple colons", arg)
105145
}
106146
}
107147
if legacySSH && len(instances) > 1 {
108-
return errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
148+
return nil, errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
109149
}
110150
scpFlags = append(scpFlags, "-3", "--")
111151
scpArgs = append(scpFlags, scpArgs...)
@@ -118,24 +158,90 @@ func copyAction(cmd *cobra.Command, args []string) error {
118158
for _, inst := range instances {
119159
sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false)
120160
if err != nil {
121-
return err
161+
return nil, err
122162
}
123163
}
124164
} else {
125165
// Copying among multiple hosts; we can't pass in host-specific options.
126166
sshOpts, err = sshutil.CommonOpts("ssh", false)
127167
if err != nil {
128-
return err
168+
return nil, err
129169
}
130170
}
131171
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
132172

133-
sshCmd := exec.Command(arg0, append(sshArgs, scpArgs...)...)
134-
sshCmd.Stdin = cmd.InOrStdin()
135-
sshCmd.Stdout = cmd.OutOrStdout()
136-
sshCmd.Stderr = cmd.ErrOrStderr()
137-
logrus.Debugf("executing scp (may take a long time): %+v", sshCmd.Args)
173+
return exec.Command(command, append(sshArgs, scpArgs...)...), nil
174+
}
138175

139-
// TODO: use syscall.Exec directly (results in losing tty?)
140-
return sshCmd.Run()
176+
func rsyncCommand(command string, args []string, verbose, recursive bool) (*exec.Cmd, error) {
177+
instances := make(map[string]*store.Instance)
178+
179+
var instName string
180+
181+
rsyncFlags := []string{}
182+
rsyncArgs := []string{}
183+
184+
if verbose {
185+
rsyncFlags = append(rsyncFlags, "-v", "--progress")
186+
} else {
187+
rsyncFlags = append(rsyncFlags, "-q")
188+
}
189+
190+
if recursive {
191+
rsyncFlags = append(rsyncFlags, "-r")
192+
}
193+
194+
for _, arg := range args {
195+
path := strings.Split(arg, ":")
196+
switch len(path) {
197+
case 1:
198+
inst, ok := instances[instName]
199+
if !ok {
200+
return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
201+
}
202+
guestVM := fmt.Sprintf("%[email protected]:%s", *inst.Config.User.Name, path[0])
203+
rsyncArgs = append(rsyncArgs, guestVM)
204+
case 2:
205+
instName = path[0]
206+
inst, err := store.Inspect(instName)
207+
if err != nil {
208+
if errors.Is(err, os.ErrNotExist) {
209+
return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
210+
}
211+
return nil, err
212+
}
213+
sshOpts, err := sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false)
214+
if err != nil {
215+
return nil, err
216+
}
217+
218+
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
219+
sshStr := fmt.Sprintf("ssh -p %s %s", fmt.Sprintf("%d", inst.SSHLocalPort), strings.Join(sshArgs, " "))
220+
221+
destDir := args[1]
222+
mkdirCmd := exec.Command(
223+
"ssh",
224+
"-p", fmt.Sprintf("%d", inst.SSHLocalPort),
225+
)
226+
mkdirCmd.Args = append(mkdirCmd.Args, sshArgs...)
227+
mkdirCmd.Args = append(mkdirCmd.Args,
228+
fmt.Sprintf("%s@%s", *inst.Config.User.Name, "127.0.0.1"),
229+
fmt.Sprintf("sudo mkdir -p %q && sudo chown %s:%s %s", destDir, *inst.Config.User.Name, *inst.Config.User.Name, destDir),
230+
)
231+
mkdirCmd.Stdout = os.Stdout
232+
mkdirCmd.Stderr = os.Stderr
233+
if err := mkdirCmd.Run(); err != nil {
234+
return nil, fmt.Errorf("failed to create directory %q on remote: %w", destDir, err)
235+
}
236+
237+
rsyncArgs = append(rsyncArgs, "-avz", "-e", sshStr, path[1])
238+
instances[instName] = inst
239+
default:
240+
return nil, fmt.Errorf("path %q contains multiple colons", arg)
241+
}
242+
}
243+
244+
rsyncArgs = append(rsyncFlags, rsyncArgs...)
245+
246+
return exec.Command(command, rsyncArgs...), nil
141247
}

hack/test-templates.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ if [ "$got" != "$expected" ]; then
192192
fi
193193

194194
INFO "Testing limactl copy command"
195-
tmpfile="$HOME/lima-hostname"
195+
tmpfile="/var/tmp/lima-hostname"
196196
rm -f "$tmpfile"
197197
limactl cp "$NAME":/etc/hostname "$tmpfile"
198198
defer "rm -f \"$tmpfile\""

pkg/hostagent/hostagent.go

+40
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,41 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
424424
return info, nil
425425
}
426426

427+
func (a *HostAgent) installPackage() error {
428+
logrus.Debugf("installing packages")
429+
430+
faScript := `#!/bin/bash
431+
if ! output=$(type rsync 2>&1); then
432+
echo "rsync is not installed. Attempting to install..."
433+
434+
# Try to install rsync based on the OS
435+
if [ -f /etc/debian_version ]; then
436+
sudo apt-get update && sudo apt-get install -y rsync
437+
elif [ -f /etc/alpine-release ]; then
438+
sudo apk add rsync
439+
elif [ -f /etc/redhat-release ]; then
440+
sudo yum install -y rsync
441+
elif [ -f /etc/arch-release ]; then
442+
sudo pacman -S --noconfirm rsync
443+
else
444+
echo "Unsupported Linux distribution. Please install rsync manually."
445+
fi
446+
447+
echo "rsync installation complete."
448+
else
449+
echo "rsync is already installed."
450+
fi`
451+
faDesc := "installing rsync"
452+
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc)
453+
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
454+
if err != nil {
455+
err = fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)
456+
return err
457+
}
458+
459+
return nil
460+
}
461+
427462
func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
428463
if *a.instConfig.Plain {
429464
logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.")
@@ -439,6 +474,11 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
439474
if err := a.waitForRequirements("essential", a.essentialRequirements()); err != nil {
440475
errs = append(errs, err)
441476
}
477+
478+
if err := a.installPackage(); err != nil {
479+
errs = append(errs, err)
480+
}
481+
442482
if *a.instConfig.SSH.ForwardAgent {
443483
faScript := `#!/bin/bash
444484
set -eux -o pipefail

0 commit comments

Comments
 (0)