Skip to content
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
13 changes: 10 additions & 3 deletions pkg/lima/files/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{{- if eq .VMType "vz" -}}
vmType: "vz"
rosetta:
enabled: false
mountType: "virtiofs"
networks:
- vzNAT: true
{{- else -}}
vmType: "qemu"
mountType: "9p"
networks:
- lima: user-v2
{{- end }}
images:
{{ range $image := .Config.Images -}}
- location: {{ $image.Location }}
Expand All @@ -12,12 +22,9 @@ mounts:
mountPoint: /srv/www/{{ $siteName }}/current
writable: true
{{ end }}
mountType: "virtiofs"
ssh:
forwardAgent: true
loadDotSSHPubKeys: true
networks:
- vzNAT: true
{{ if .Config.PortForwards }}
portForwards:
{{ range $port := .Config.PortForwards -}}
Expand Down
26 changes: 21 additions & 5 deletions pkg/lima/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Instance struct {
SshLocalPort int `json:"sshLocalPort,omitempty"`
Config Config `json:"config"`
Username string `json:"username,omitempty"`
VMType string `json:"vmType,omitempty"` // "vz" for macOS, "qemu" for Linux
}

func (i *Instance) ConfigFile() string {
Expand Down Expand Up @@ -111,15 +112,30 @@ Gets the IP address of the instance using the output of `ip route`:
default via 192.168.64.1 proto dhcp src 192.168.64.2 metric 100
192.168.64.0/24 proto kernel scope link src 192.168.64.2
192.168.64.1 proto dhcp scope link src 192.168.64.2 metric 100

On Linux with QEMU, the interface name varies (eth0, enp0s1, etc.) so we use
the default route without specifying a device.
*/
func (i *Instance) IP() (ip string, err error) {
output, err := command.Cmd(
"limactl",
[]string{"shell", "--workdir", "/", i.Name, "ip", "route", "show", "dev", "lima0"},
).CombinedOutput()
// Try lima0 first (macOS vz), then fall back to default route (Linux QEMU)
args := []string{"shell", "--workdir", "/", i.Name, "ip", "route", "show", "dev", "lima0"}
output, err := command.Cmd("limactl", args).CombinedOutput()

if err != nil {
return "", fmt.Errorf("%w: %v\n%s", IpErr, err, string(output))
// lima0 doesn't exist (likely Linux QEMU), try getting default route
args = []string{"shell", "--workdir", "/", i.Name, "ip", "route", "get", "1.1.1.1"}
output, err = command.Cmd("limactl", args).CombinedOutput()
if err != nil {
return "", fmt.Errorf("%w: %v\n%s", IpErr, err, string(output))
}

// Parse output like: "1.1.1.1 via 10.0.2.2 dev eth0 src 10.0.2.15 uid 1000"
re := regexp.MustCompile(`src ([0-9\.]+)`)
matches := re.FindStringSubmatch(string(output))
if len(matches) < 2 {
return "", fmt.Errorf("%w: no IP address could be matched in the ip route output\n%s", IpErr, string(output))
}
return matches[1], nil
}

re := regexp.MustCompile(`default via .* src ([0-9\.]+)`)
Expand Down
97 changes: 89 additions & 8 deletions pkg/lima/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/roots/trellis-cli/command"
Expand All @@ -19,8 +20,15 @@ func TestGenerateConfig(t *testing.T) {

dir := t.TempDir()

// Set VMType based on OS, as newInstance() does
vmType := "qemu"
if runtime.GOOS == "darwin" {
vmType = "vz"
}

instance := &Instance{
Dir: dir,
Dir: dir,
VMType: vmType,
Config: Config{
Images: []Image{
{
Expand All @@ -45,9 +53,14 @@ func TestGenerateConfig(t *testing.T) {

absSitePath := filepath.Join(trellis.Path, "../site")

expected := fmt.Sprintf(`vmType: "vz"
var expected string
if runtime.GOOS == "darwin" {
expected = fmt.Sprintf(`vmType: "vz"
rosetta:
enabled: false
mountType: "virtiofs"
networks:
- vzNAT: true
images:
- location: http://ubuntu.com/focal
arch: aarch64
Expand All @@ -57,12 +70,39 @@ mounts:
mountPoint: /srv/www/example.com/current
writable: true

mountType: "virtiofs"
ssh:
forwardAgent: true
loadDotSSHPubKeys: true

portForwards:
- guestPort: 80
hostPort: 1234

containerd:
user: false
provision:
- mode: system
script: |
#!/bin/bash
echo "127.0.0.1 $(hostname)" >> /etc/hosts
`, absSitePath)
} else {
expected = fmt.Sprintf(`vmType: "qemu"
mountType: "9p"
networks:
- vzNAT: true
- lima: user-v2
images:
- location: http://ubuntu.com/focal
arch: aarch64

mounts:
- location: %s
mountPoint: /srv/www/example.com/current
writable: true

ssh:
forwardAgent: true
loadDotSSHPubKeys: true

portForwards:
- guestPort: 80
Expand All @@ -76,6 +116,7 @@ provision:
#!/bin/bash
echo "127.0.0.1 $(hostname)" >> /etc/hosts
`, absSitePath)
}

if content.String() != expected {
t.Errorf("expected %s\ngot %s", expected, content.String())
Expand All @@ -91,8 +132,15 @@ func TestUpdateConfig(t *testing.T) {

dir := t.TempDir()

// Set VMType based on OS, as newInstance() does
vmType := "qemu"
if runtime.GOOS == "darwin" {
vmType = "vz"
}

instance := &Instance{
Dir: dir,
Dir: dir,
VMType: vmType,
Config: Config{
Images: []Image{
{
Expand Down Expand Up @@ -123,9 +171,14 @@ func TestUpdateConfig(t *testing.T) {

absSitePath := filepath.Join(trellis.Path, "../site")

expected := fmt.Sprintf(`vmType: "vz"
var expected string
if runtime.GOOS == "darwin" {
expected = fmt.Sprintf(`vmType: "vz"
rosetta:
enabled: false
mountType: "virtiofs"
networks:
- vzNAT: true
images:
- location: http://ubuntu.com/focal
arch: aarch64
Expand All @@ -135,12 +188,39 @@ mounts:
mountPoint: /srv/www/example.com/current
writable: true

mountType: "virtiofs"
ssh:
forwardAgent: true
loadDotSSHPubKeys: true

portForwards:
- guestPort: 80
hostPort: 1234

containerd:
user: false
provision:
- mode: system
script: |
#!/bin/bash
echo "127.0.0.1 $(hostname)" >> /etc/hosts
`, absSitePath)
} else {
expected = fmt.Sprintf(`vmType: "qemu"
mountType: "9p"
networks:
- vzNAT: true
- lima: user-v2
images:
- location: http://ubuntu.com/focal
arch: aarch64

mounts:
- location: %s
mountPoint: /srv/www/example.com/current
writable: true

ssh:
forwardAgent: true
loadDotSSHPubKeys: true

portForwards:
- guestPort: 80
Expand All @@ -154,6 +234,7 @@ provision:
#!/bin/bash
echo "127.0.0.1 $(hostname)" >> /etc/hosts
`, absSitePath)
}

if string(content) != expected {
t.Errorf("expected %s\ngot %s", expected, string(content))
Expand Down
11 changes: 9 additions & 2 deletions pkg/lima/lima.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ func Installed() error {

output, err := command.Cmd("limactl", []string{"-v"}).Output()
if err != nil {
return fmt.Errorf("Could get determine the version of Lima.")
return fmt.Errorf("Could not determine the version of Lima.")
}

re := regexp.MustCompile(`.*([0-9]+\.[0-9]+\.[0-9]+(-alpha|beta)?)`)
re := regexp.MustCompile(`([0-9]+\.[0-9]+\.[0-9]+(-alpha|-beta)?)`)
v := re.FindStringSubmatch(string(output))

// If no semantic version found (e.g., git hash on Linux distro packages),
// assume it's a recent enough version since distro packages are typically up-to-date
if len(v) < 2 {
return nil
}

constraint := version.NewConstrainGroupFromString(VersionRequired)
matched := constraint.Match(v[1])

Expand Down
21 changes: 20 additions & 1 deletion pkg/lima/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/fatih/color"
Expand All @@ -26,7 +27,7 @@ const (

var (
ErrConfigPath = errors.New("could not create config directory")
ErrUnsupportedOS = errors.New("unsupported OS or macOS version. The macOS Virtualization Framework requires macOS 13.0 (Ventura) or later.")
ErrUnsupportedOS = errors.New("unsupported OS version. Lima on macOS requires macOS 13.0 (Ventura) or later.")
)

type PortFinder interface {
Expand Down Expand Up @@ -299,6 +300,14 @@ func (m *Manager) initInstance(instance *Instance) {

func (m *Manager) newInstance(name string) (Instance, error) {
instance := Instance{Name: name}

// Set VMType based on OS: "vz" for macOS (Virtualization.framework), "qemu" for Linux
if runtime.GOOS == "darwin" {
instance.VMType = "vz"
} else {
instance.VMType = "qemu"
}

m.initInstance(&instance)

images := []Image{}
Expand Down Expand Up @@ -390,6 +399,16 @@ func getMacOSVersion() (string, error) {
}

func ensureRequirements() error {
if runtime.GOOS == "linux" {
// Linux doesn't have macOS version requirements
// Just check that Lima is installed
if err := Installed(); err != nil {
return fmt.Errorf("%s\nInstall or upgrade Lima to continue.\n\nSee https://lima-vm.io/docs/installation/ for installation options.", err.Error())
}
return nil
}

// macOS requirements
macOSVersion, err := getMacOSVersion()
if err != nil {
return ErrUnsupportedOS
Expand Down
41 changes: 29 additions & 12 deletions pkg/lima/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/hashicorp/cli"
Expand Down Expand Up @@ -34,17 +35,28 @@ func TestNewManager(t *testing.T) {
path := os.Getenv("PATH")
t.Setenv("PATH", fmt.Sprintf("PATH=%s:%s", path, tmp))

commands := []command.MockCommand{
{
Command: "sw_vers",
Args: []string{"-productVersion"},
Output: `13.0.1`,
},
{
Command: "limactl",
Args: []string{"-v"},
Output: `limactl version 0.15.0`,
},
var commands []command.MockCommand
if runtime.GOOS == "darwin" {
commands = []command.MockCommand{
{
Command: "sw_vers",
Args: []string{"-productVersion"},
Output: `13.0.1`,
},
{
Command: "limactl",
Args: []string{"-v"},
Output: `limactl version 0.15.0`,
},
}
} else {
commands = []command.MockCommand{
{
Command: "limactl",
Args: []string{"-v"},
Output: `limactl version 0.15.0`,
},
}
}
defer command.MockExecCommands(t, commands)()

Expand All @@ -55,6 +67,11 @@ func TestNewManager(t *testing.T) {
}

func TestNewManagerUnsupportedOS(t *testing.T) {
// This test is macOS-specific (tests for old macOS version rejection)
if runtime.GOOS != "darwin" {
t.Skip("Skipping macOS-specific test on non-macOS platform")
}

defer trellis.LoadFixtureProject(t)()
trellis := trellis.NewTrellis()
if err := trellis.LoadProject(); err != nil {
Expand All @@ -81,7 +98,7 @@ func TestNewManagerUnsupportedOS(t *testing.T) {
t.Fatal(err)
}

expected := "unsupported OS or macOS version. The macOS Virtualization Framework requires macOS 13.0 (Ventura) or later."
expected := "unsupported OS version. Lima on macOS requires macOS 13.0 (Ventura) or later."

if err.Error() != expected {
t.Errorf("expected error to be %q, got %q", expected, err.Error())
Expand Down
2 changes: 1 addition & 1 deletion trellis/trellis.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ func (t *Trellis) WriteYamlFile(s interface{}, path string, header string) error
func (t *Trellis) VmManagerType() string {
switch t.CliConfig.Vm.Manager {
case "auto":
if runtime.GOOS == "darwin" {
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
return "lima"
}
return ""
Expand Down
Loading