diff --git a/cli_config/cli_config.go b/cli_config/cli_config.go index 5a9fdfd5..a2c3c7af 100644 --- a/cli_config/cli_config.go +++ b/cli_config/cli_config.go @@ -17,11 +17,12 @@ type VmImage struct { } type VmConfig struct { - Manager string `yaml:"manager"` - HostsResolver string `yaml:"hosts_resolver"` - Images []VmImage `yaml:"images"` - Ubuntu string `yaml:"ubuntu"` - InstanceName string `yaml:"instance_name"` + Manager string `yaml:"manager"` + HostsResolver string `yaml:"hosts_resolver"` + Images []VmImage `yaml:"images"` + Ubuntu string `yaml:"ubuntu"` + InstanceName string `yaml:"instance_name"` + ForwardHttpPort bool `yaml:"forward_http_port"` } type Config struct { diff --git a/pkg/lima/manager.go b/pkg/lima/manager.go index a1d2276e..3c9c8cf2 100644 --- a/pkg/lima/manager.go +++ b/pkg/lima/manager.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "os" "path/filepath" "strings" @@ -27,10 +28,17 @@ var ( ErrUnsupportedOS = errors.New("unsupported OS or macOS version. The macOS Virtualization Framework requires macOS 13.0 (Ventura) or later.") ) +type PortFinder interface { + Resolve() (int, error) +} + +type TCPPortFinder struct{} + type Manager struct { ConfigPath string Sites map[string]*trellis.Site HostsResolver vm.HostsResolver + PortFinder PortFinder ui cli.Ui trellis *trellis.Trellis } @@ -55,6 +63,7 @@ func NewManager(trellis *trellis.Trellis, ui cli.Ui) (manager *Manager, err erro ConfigPath: limaConfigPath, Sites: trellis.Environments["development"].WordPressSites, HostsResolver: hostsResolver, + PortFinder: &TCPPortFinder{}, trellis: trellis, ui: ui, } @@ -78,7 +87,10 @@ func (m *Manager) GetInstance(name string) (Instance, bool) { } func (m *Manager) CreateInstance(name string) error { - instance := m.newInstance(name) + instance, err := m.newInstance(name) + if err != nil { + return err + } cmd := command.WithOptions( command.WithTermOutput(), @@ -172,7 +184,8 @@ func (m *Manager) StartInstance(name string) error { instance.Username = string(user) - // Hydrate instance with data from limactl that is only available after starting (mainly the forwarded SSH local port) + // Hydrate instance with data from limactl that is only available after + // starting (mainly the forwarded local ports) err = m.hydrateInstance(&instance) if err != nil { return err @@ -232,7 +245,7 @@ func (m *Manager) initInstance(instance *Instance) { instance.Sites = m.Sites } -func (m *Manager) newInstance(name string) Instance { +func (m *Manager) newInstance(name string) (Instance, error) { instance := Instance{Name: name} m.initInstance(&instance) @@ -249,9 +262,24 @@ func (m *Manager) newInstance(name string) Instance { images = imagesFromVersion(m.trellis.CliConfig.Vm.Ubuntu) } - config := Config{Images: images} + portForwards := []PortForward{} + + if m.trellis.CliConfig.Vm.ForwardHttpPort { + httpForwardPort, err := m.PortFinder.Resolve() + if err != nil { + return Instance{}, fmt.Errorf("Could not find a local free port for HTTP forwarding: %v", err) + } + + portForwards = append(portForwards, PortForward{ + GuestPort: 80, + HostPort: httpForwardPort, + }, + ) + } + + config := Config{Images: images, PortForwards: portForwards} instance.Config = config - return instance + return instance, nil } func (m *Manager) createConfigPath() error { @@ -329,3 +357,31 @@ func ensureRequirements() error { func imagesFromVersion(version string) []Image { return UbuntuImages[version] } + +func (p *TCPPortFinder) Resolve() (int, error) { + lAddr0, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp4", lAddr0) + if err != nil { + return 0, err + } + + defer func() { _ = l.Close() }() + lAddr := l.Addr() + + lTCPAddr, ok := lAddr.(*net.TCPAddr) + if !ok { + return 0, fmt.Errorf("expected *net.TCPAddr, got %v", lAddr) + } + + port := lTCPAddr.Port + + if port <= 0 { + return 0, fmt.Errorf("unexpected port %d", port) + } + + return port, nil +} diff --git a/pkg/lima/manager_test.go b/pkg/lima/manager_test.go index de22a161..e8ca27a7 100644 --- a/pkg/lima/manager_test.go +++ b/pkg/lima/manager_test.go @@ -15,6 +15,12 @@ type MockHostsResolver struct { Hosts map[string]string } +type MockPortFinder struct{} + +func (p *MockPortFinder) Resolve() (int, error) { + return 60720, nil +} + func TestNewManager(t *testing.T) { defer trellis.LoadFixtureProject(t)() trellis := trellis.NewTrellis() @@ -120,7 +126,13 @@ func TestNewInstanceUbuntuVersion(t *testing.T) { t.Fatal(err) } - instance := manager.newInstance("test") + manager.PortFinder = &MockPortFinder{} + + instance, err := manager.newInstance("test") + + if err != nil { + t.Fatal(err) + } if instance.Name != "test" { t.Errorf("expected instance name to be %q, got %q", "test", instance.Name) @@ -133,6 +145,14 @@ func TestNewInstanceUbuntuVersion(t *testing.T) { if instance.Config.Images[0].Alias != "focal" { t.Errorf("expected instance config to have focal image, got %q", instance.Config.Images[0].Alias) } + + if len(instance.Config.PortForwards) != 1 { + t.Errorf("expected instance config to have 1 port forwards, got %d", len(instance.Config.PortForwards)) + } + + if instance.Config.PortForwards[0].GuestPort != 80 || instance.Config.PortForwards[0].HostPort != 60720 { + t.Errorf("expected instance config to have port forward guest 80 to host 60720, got guest %d to host %d", instance.Config.PortForwards[0].GuestPort, instance.Config.PortForwards[0].HostPort) + } } func TestInstances(t *testing.T) { defer trellis.LoadFixtureProject(t)() @@ -197,6 +217,7 @@ func TestCreateInstance(t *testing.T) { hostsStorage := make(map[string]string) manager.HostsResolver = &MockHostsResolver{Hosts: hostsStorage} + manager.PortFinder = &MockPortFinder{} instanceName := "test" sshPort := 60720 diff --git a/trellis/trellis.go b/trellis/trellis.go index 27e508e7..fda20807 100644 --- a/trellis/trellis.go +++ b/trellis/trellis.go @@ -36,10 +36,11 @@ var DefaultCliConfig = cli_config.Config{ Open: make(map[string]string), VirtualenvIntegration: true, Vm: cli_config.VmConfig{ - Manager: "auto", - HostsResolver: "hosts_file", - Ubuntu: "24.04", - InstanceName: "", + Manager: "auto", + HostsResolver: "hosts_file", + Ubuntu: "24.04", + InstanceName: "", + ForwardHttpPort: true, }, }