From 8faba6e144acbeaf35f6022994287f801e02552b Mon Sep 17 00:00:00 2001 From: Scott Walkinshaw Date: Sun, 18 Oct 2026 11:45:21 -0400 Subject: [PATCH] Add vm.mount_type config option Allows overriding the Lima mount type (9p, virtiofs, reverse-sshfs) for site directories. Defaults are unchanged: virtiofs for vz (macOS) and 9p for qemu (Linux). The 9p securityModel is only set when using 9p. Ref #670 Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + cli_config/cli_config.go | 5 ++++ cli_config/cli_config_test.go | 43 +++++++++++++++++++++++++++++ pkg/lima/files/config.yml | 8 ++---- pkg/lima/instance.go | 15 ++++++++++ pkg/lima/instance_test.go | 52 +++++++++++++++++++++++++++++++---- pkg/lima/manager.go | 1 + pkg/lima/manager_test.go | 24 ++++++++++++++++ 8 files changed, 138 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6da3b231..0b0c8f13 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ Current supported settings: | `hosts_resolver` | VM hosts resolver (Options: `hosts_file`)| string | | `instance_name` | Custom name for the VM instance | string | First site name alphabetically | | `images` | Custom OS image | object | Set based on `ubuntu` version | +| `mount_type` | Lima mount type for site directories (Options: `9p`, `virtiofs`, `reverse-sshfs`) | string | `virtiofs` on macOS, `9p` on Linux | #### `images` | Setting | Description | Type | Default | diff --git a/cli_config/cli_config.go b/cli_config/cli_config.go index afbc14c6..61d30795 100644 --- a/cli_config/cli_config.go +++ b/cli_config/cli_config.go @@ -23,6 +23,7 @@ type VmConfig struct { Ubuntu string `yaml:"ubuntu"` InstanceName string `yaml:"instance_name"` ForwardHttpPort bool `yaml:"forward_http_port"` + MountType string `yaml:"mount_type"` } type ServerConfig struct { @@ -74,6 +75,10 @@ func (c *Config) LoadFile(path string) error { return fmt.Errorf("%w: unsupported value for `vm.hosts_resolver`. Must be one of: hosts_file", InvalidConfigErr) } + if c.Vm.MountType != "" && c.Vm.MountType != "9p" && c.Vm.MountType != "virtiofs" && c.Vm.MountType != "reverse-sshfs" { + return fmt.Errorf("%w: unsupported value for `vm.mount_type`. Must be one of: 9p, virtiofs, reverse-sshfs", InvalidConfigErr) + } + if c.DatabaseApp != "" && c.DatabaseApp != "tableplus" && c.DatabaseApp != "sequel-ace" { return fmt.Errorf("%w: unsupported value for `database_app`. Must be one of: tableplus, sequel-ace", InvalidConfigErr) } diff --git a/cli_config/cli_config_test.go b/cli_config/cli_config_test.go index a63d0f9b..b901a751 100644 --- a/cli_config/cli_config_test.go +++ b/cli_config/cli_config_test.go @@ -46,6 +46,49 @@ open: } } +func TestLoadFileMountType(t *testing.T) { + conf := Config{} + + dir := t.TempDir() + path := filepath.Join(dir, "cli.yml") + content := ` +vm: + mount_type: virtiofs +` + + if err := os.WriteFile(path, []byte(content), os.ModePerm); err != nil { + t.Fatal(err) + } + + if err := conf.LoadFile(path); err != nil { + t.Fatal(err) + } + + if conf.Vm.MountType != "virtiofs" { + t.Errorf("expected Vm.MountType to be %q, got %q", "virtiofs", conf.Vm.MountType) + } +} + +func TestLoadFileInvalidMountType(t *testing.T) { + conf := Config{} + + dir := t.TempDir() + path := filepath.Join(dir, "cli.yml") + content := ` +vm: + mount_type: nfs +` + + if err := os.WriteFile(path, []byte(content), os.ModePerm); err != nil { + t.Fatal(err) + } + + err := conf.LoadFile(path) + if err == nil || !strings.Contains(err.Error(), "vm.mount_type") { + t.Errorf("expected invalid vm.mount_type error, got %v", err) + } +} + func TestLoadEnv(t *testing.T) { t.Setenv("TRELLIS_ASK_VAULT_PASS", "true") t.Setenv("TRELLIS_NOPE", "foo") diff --git a/pkg/lima/files/config.yml b/pkg/lima/files/config.yml index 856ba8e3..afa917e1 100644 --- a/pkg/lima/files/config.yml +++ b/pkg/lima/files/config.yml @@ -13,16 +13,12 @@ mounts: - location: {{ $site.AbsLocalPath }} mountPoint: /srv/www/{{ $siteName }}/current writable: true -{{- if eq $.VMType "qemu" }} +{{- if eq $.ResolvedMountType "9p" }} 9p: securityModel: "mapped-xattr" {{- end }} {{ end }} -{{- if eq .VMType "vz" }} -mountType: "virtiofs" -{{- else }} -mountType: "9p" -{{- end }} +mountType: "{{ .ResolvedMountType }}" ssh: forwardAgent: true loadDotSSHPubKeys: true diff --git a/pkg/lima/instance.go b/pkg/lima/instance.go index 14e21a97..1c719f2c 100644 --- a/pkg/lima/instance.go +++ b/pkg/lima/instance.go @@ -53,6 +53,7 @@ type Instance struct { Disk int `json:"disk"` SshLocalPort int `json:"sshLocalPort,omitempty"` VMType string `json:"vmType,omitempty"` + MountType string `json:"-"` Config Config `json:"config"` Username string `json:"username,omitempty"` } @@ -61,6 +62,20 @@ func (i *Instance) ConfigFile() string { return filepath.Join(i.Dir, "lima.yaml") } +// ResolvedMountType returns the configured mount type, falling back to the +// default for the VM type (virtiofs for vz, 9p for qemu). +func (i *Instance) ResolvedMountType() string { + if i.MountType != "" { + return i.MountType + } + + if i.VMType == "vz" { + return "virtiofs" + } + + return "9p" +} + func (i *Instance) GenerateConfig() (*bytes.Buffer, error) { var contents bytes.Buffer diff --git a/pkg/lima/instance_test.go b/pkg/lima/instance_test.go index f7f777c0..9a7f0bbb 100644 --- a/pkg/lima/instance_test.go +++ b/pkg/lima/instance_test.go @@ -21,9 +21,10 @@ func TestGenerateConfig(t *testing.T) { absSitePath := filepath.Join(trellis.Path, "../site") testCases := []struct { - name string - vmType string - expected string + name string + vmType string + mountType string + expected string }{ { name: "vz", @@ -82,6 +83,46 @@ ssh: networks: - lima: user-v2 +portForwards: +- guestPort: 80 + hostPort: 1234 + +containerd: + user: false +provision: +- mode: system + script: | + #!/bin/bash + echo "127.0.0.1 $(hostname)" >> /etc/hosts + + TAP_IFACE="$(ip -o link show | awk '/52:54:00:12:34:56/ {print $2}' | tr -d ':' | head -n1)" + if [ -n "$TAP_IFACE" ]; then + ip link set "$TAP_IFACE" up + ip addr add 192.168.56.5/24 dev "$TAP_IFACE" 2>/dev/null || true + fi +`, absSitePath), + }, + { + name: "qemu with virtiofs", + vmType: "qemu", + mountType: "virtiofs", + expected: fmt.Sprintf(`vmType: "qemu" +images: +- location: http://ubuntu.com/focal + arch: aarch64 + +mounts: +- location: %s + mountPoint: /srv/www/example.com/current + writable: true + +mountType: "virtiofs" +ssh: + forwardAgent: true + loadDotSSHPubKeys: true +networks: +- lima: user-v2 + portForwards: - guestPort: 80 hostPort: 1234 @@ -106,8 +147,9 @@ provision: for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { instance := &Instance{ - Dir: dir, - VMType: tc.vmType, + Dir: dir, + VMType: tc.vmType, + MountType: tc.mountType, Config: Config{ Images: []Image{ { diff --git a/pkg/lima/manager.go b/pkg/lima/manager.go index e7712fab..3393dd57 100644 --- a/pkg/lima/manager.go +++ b/pkg/lima/manager.go @@ -352,6 +352,7 @@ func (m *Manager) hydrateInstance(instance *Instance) error { func (m *Manager) initInstance(instance *Instance) { instance.InventoryFile = m.InventoryPath() instance.Sites = m.Sites + instance.MountType = m.trellis.CliConfig.Vm.MountType } func (m *Manager) newInstance(name string) (Instance, error) { diff --git a/pkg/lima/manager_test.go b/pkg/lima/manager_test.go index d8e5b655..dca800fa 100644 --- a/pkg/lima/manager_test.go +++ b/pkg/lima/manager_test.go @@ -120,6 +120,30 @@ func TestInitInstance(t *testing.T) { } } +func TestInitInstanceMountType(t *testing.T) { + defer trellis.LoadFixtureProject(t)() + trellis := trellis.NewTrellis() + if err := trellis.LoadProject(); err != nil { + t.Fatal(err) + } + + trellis.CliConfig.Vm.MountType = "virtiofs" + + t.Setenv("TRELLIS_BYPASS_LIMA_REQUIREMENTS", "1") + + manager, err := NewManager(trellis, cli.NewMockUi()) + if err != nil { + t.Fatal(err) + } + + instance := Instance{Name: "test", VMType: "qemu"} + manager.initInstance(&instance) + + if instance.ResolvedMountType() != "virtiofs" { + t.Errorf("expected mount type to be %q, got %q", "virtiofs", instance.ResolvedMountType()) + } +} + func TestNewInstanceUbuntuVersion(t *testing.T) { defer trellis.LoadFixtureProject(t)() trellis := trellis.NewTrellis()