diff --git a/cmd/limactl/guest-install.go b/cmd/limactl/guest-install.go new file mode 100644 index 00000000000..82e5290a335 --- /dev/null +++ b/cmd/limactl/guest-install.go @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + + "github.com/lima-vm/lima/pkg/cacheutil" + "github.com/lima-vm/lima/pkg/store" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/lima-vm/lima/pkg/usrlocalsharelima" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func newGuestInstallCommand() *cobra.Command { + guestInstallCommand := &cobra.Command{ + Use: "guest-install INSTANCE", + Short: "Install guest components", + Args: WrapArgsError(cobra.MaximumNArgs(1)), + RunE: guestInstallAction, + ValidArgsFunction: cobra.NoFileCompletions, + Hidden: true, + } + return guestInstallCommand +} + +func runCmd(name string, flags []string, args ...string) error { + cmd := exec.Command(name, append(flags, args...)...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + logrus.Debugf("executing %v", cmd.Args) + return cmd.Run() +} + +func shell(name string, flags []string, args ...string) (string, error) { + cmd := exec.Command(name, append(flags, args...)...) + out, err := cmd.Output() + if err != nil { + return "", err + } + out = bytes.TrimSuffix(out, []byte{'\n'}) + return string(out), nil +} + +func guestInstallAction(cmd *cobra.Command, args []string) error { + instName := DefaultInstanceName + if len(args) > 0 { + instName = args[0] + } + + inst, err := store.Inspect(instName) + if err != nil { + return err + } + if inst.Status == store.StatusStopped { + return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) + } + + sshExe := "ssh" + sshConfig := filepath.Join(inst.Dir, filenames.SSHConfig) + sshFlags := []string{"-F", sshConfig} + + scpExe := "scp" + scpFlags := sshFlags + + hostname := fmt.Sprintf("lima-%s", inst.Name) + prefix := *inst.Config.GuestInstallPrefix + + // lima-guestagent + guestAgentBinary, err := usrlocalsharelima.GuestAgentBinary(*inst.Config.OS, *inst.Config.Arch) + if err != nil { + return err + } + guestAgentFilename := filepath.Base(guestAgentBinary) + if _, err := os.Stat(guestAgentBinary); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + compressedGuestAgent, err := os.Open(guestAgentBinary + ".gz") + if err != nil { + return err + } + defer compressedGuestAgent.Close() + tmpGuestAgent, err := os.CreateTemp("", "lima-guestagent-") + if err != nil { + return err + } + logrus.Debugf("Decompressing %s.gz", guestAgentBinary) + guestAgent, err := gzip.NewReader(compressedGuestAgent) + if err != nil { + return err + } + defer guestAgent.Close() + _, err = io.Copy(tmpGuestAgent, guestAgent) + if err != nil { + return err + } + tmpGuestAgent.Close() + guestAgentBinary = tmpGuestAgent.Name() + defer os.RemoveAll(guestAgentBinary) + } + tmpname := "lima-guestagent" + tmp, err := shell(sshExe, sshFlags, hostname, "mktemp", "-t", "lima-guestagent.XXXXXX") + if err != nil { + return err + } + bin := prefix + "/bin/lima-guestagent" + logrus.Infof("Copying %q to %s:%s", guestAgentFilename, inst.Name, tmpname) + scpArgs := []string{guestAgentBinary, hostname + ":" + tmp} + if err := runCmd(scpExe, scpFlags, scpArgs...); err != nil { + return nil + } + logrus.Infof("Installing %s to %s", tmpname, bin) + sshArgs := []string{hostname, "sudo", "install", "-m", "755", tmp, bin} + if err := runCmd(sshExe, sshFlags, sshArgs...); err != nil { + return nil + } + _, _ = shell(sshExe, sshFlags, hostname, "rm", tmp) + + // nerdctl-full.tgz + nerdctlFilename := cacheutil.NerdctlArchive(inst.Config) + if nerdctlFilename != "" { + nerdctlArchive, err := cacheutil.EnsureNerdctlArchiveCache(cmd.Context(), inst.Config, false) + if err != nil { + return err + } + tmpname := "nerdctl-full.tgz" + tmp, err := shell(sshExe, sshFlags, hostname, "mktemp", "-t", "nerdctl-full.XXXXXX.tgz") + if err != nil { + return err + } + logrus.Infof("Copying %q to %s:%s", nerdctlFilename, inst.Name, tmpname) + scpArgs := []string{nerdctlArchive, hostname + ":" + tmp} + if err := runCmd(scpExe, scpFlags, scpArgs...); err != nil { + return nil + } + logrus.Infof("Installing %s in %s", tmpname, prefix) + sshArgs := []string{hostname, "sudo", "tar", "Cxzf", prefix, tmp} + if err := runCmd(sshExe, sshFlags, sshArgs...); err != nil { + return nil + } + _, _ = shell(sshExe, sshFlags, hostname, "rm", tmp) + } + + return nil +} diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go index a428a9fd875..bd89d8b0333 100644 --- a/cmd/limactl/main.go +++ b/cmd/limactl/main.go @@ -171,6 +171,7 @@ func newApp() *cobra.Command { newValidateCommand(), newPruneCommand(), newHostagentCommand(), + newGuestInstallCommand(), newInfoCommand(), newShowSSHCommand(), newDebugCommand(), diff --git a/pkg/cacheutil/cacheutil.go b/pkg/cacheutil/cacheutil.go new file mode 100644 index 00000000000..c2c3f4af35c --- /dev/null +++ b/pkg/cacheutil/cacheutil.go @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package cacheutil + +import ( + "context" + "fmt" + "path" + + "github.com/lima-vm/lima/pkg/downloader" + "github.com/lima-vm/lima/pkg/fileutils" + "github.com/lima-vm/lima/pkg/limayaml" +) + +// NerdctlArchive returns the basename of the archive. +func NerdctlArchive(y *limayaml.LimaYAML) string { + if *y.Containerd.System || *y.Containerd.User { + for _, f := range y.Containerd.Archives { + if f.Arch == *y.Arch { + return path.Base(f.Location) + } + } + } + return "" +} + +// EnsureNerdctlArchiveCache prefetches the nerdctl-full-VERSION-GOOS-GOARCH.tar.gz archive +// into the cache before launching the hostagent process, so that we can show the progress in tty. +// https://github.com/lima-vm/lima/issues/326 +func EnsureNerdctlArchiveCache(ctx context.Context, y *limayaml.LimaYAML, created bool) (string, error) { + if !*y.Containerd.System && !*y.Containerd.User { + // nerdctl archive is not needed + return "", nil + } + + errs := make([]error, len(y.Containerd.Archives)) + for i, f := range y.Containerd.Archives { + // Skip downloading again if the file is already in the cache + if created && f.Arch == *y.Arch && !downloader.IsLocal(f.Location) { + path, err := fileutils.CachedFile(f) + if err == nil { + return path, nil + } + } + path, err := fileutils.DownloadFile(ctx, "", f, false, "the nerdctl archive", *y.Arch) + if err != nil { + errs[i] = err + continue + } + if path == "" { + if downloader.IsLocal(f.Location) { + return f.Location, nil + } + return "", fmt.Errorf("cache did not contain %q", f.Location) + } + return path, nil + } + + return "", fileutils.Errors(errs) +} diff --git a/pkg/instance/start.go b/pkg/instance/start.go index b53de08183a..52bddd17ff3 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -18,6 +18,7 @@ import ( "time" "github.com/coreos/go-semver/semver" + "github.com/lima-vm/lima/pkg/cacheutil" "github.com/lima-vm/lima/pkg/driver" "github.com/lima-vm/lima/pkg/driverutil" "github.com/lima-vm/lima/pkg/executil" @@ -26,8 +27,6 @@ import ( "github.com/lima-vm/lima/pkg/qemu/entitlementutil" "github.com/mattn/go-isatty" - "github.com/lima-vm/lima/pkg/downloader" - "github.com/lima-vm/lima/pkg/fileutils" hostagentevents "github.com/lima-vm/lima/pkg/hostagent/events" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/store" @@ -39,41 +38,6 @@ import ( // to be running before timing out. const DefaultWatchHostAgentEventsTimeout = 10 * time.Minute -// ensureNerdctlArchiveCache prefetches the nerdctl-full-VERSION-GOOS-GOARCH.tar.gz archive -// into the cache before launching the hostagent process, so that we can show the progress in tty. -// https://github.com/lima-vm/lima/issues/326 -func ensureNerdctlArchiveCache(ctx context.Context, y *limayaml.LimaYAML, created bool) (string, error) { - if !*y.Containerd.System && !*y.Containerd.User { - // nerdctl archive is not needed - return "", nil - } - - errs := make([]error, len(y.Containerd.Archives)) - for i, f := range y.Containerd.Archives { - // Skip downloading again if the file is already in the cache - if created && f.Arch == *y.Arch && !downloader.IsLocal(f.Location) { - path, err := fileutils.CachedFile(f) - if err == nil { - return path, nil - } - } - path, err := fileutils.DownloadFile(ctx, "", f, false, "the nerdctl archive", *y.Arch) - if err != nil { - errs[i] = err - continue - } - if path == "" { - if downloader.IsLocal(f.Location) { - return f.Location, nil - } - return "", fmt.Errorf("cache did not contain %q", f.Location) - } - return path, nil - } - - return "", fileutils.Errors(errs) -} - type Prepared struct { Driver driver.Driver NerdctlArchiveCache string @@ -101,7 +65,7 @@ func Prepare(ctx context.Context, inst *store.Instance) (*Prepared, error) { if err := limaDriver.CreateDisk(ctx); err != nil { return nil, err } - nerdctlArchiveCache, err := ensureNerdctlArchiveCache(ctx, inst.Config, created) + nerdctlArchiveCache, err := cacheutil.EnsureNerdctlArchiveCache(ctx, inst.Config, created) if err != nil { return nil, err }