diff --git a/cmd/vm_start.go b/cmd/vm_start.go index d694dbfe..3c2128eb 100644 --- a/cmd/vm_start.go +++ b/cmd/vm_start.go @@ -73,13 +73,19 @@ func (c *VmStartCommand) Run(args []string) int { return 1 } - // VM doesn't exist yet, create it + // VM doesn't exist yet, create and start it if err = manager.CreateInstance(siteName); err != nil { c.UI.Error("Error creating VM.") c.UI.Error(err.Error()) return 1 } + if err = manager.StartInstance(siteName); err != nil { + c.UI.Error("Error starting VM.") + c.UI.Error(err.Error()) + return 1 + } + c.UI.Info("\nProvisioning VM...") provisionCmd := NewProvisionCommand(c.UI, c.Trellis) diff --git a/pkg/lima/instance.go b/pkg/lima/instance.go index 48368b96..8b32bcd5 100644 --- a/pkg/lima/instance.go +++ b/pkg/lima/instance.go @@ -1,10 +1,12 @@ package lima import ( + "bytes" _ "embed" "errors" "fmt" "os" + "path/filepath" "regexp" "text/template" @@ -40,7 +42,6 @@ type Config struct { } type Instance struct { - ConfigFile string InventoryFile string Sites map[string]*trellis.Site Name string `json:"name"` @@ -55,16 +56,29 @@ type Instance struct { Username string `json:"username,omitempty"` } -func (i *Instance) CreateConfig() error { +func (i *Instance) ConfigFile() string { + return filepath.Join(i.Dir, "lima.yaml") +} + +func (i *Instance) GenerateConfig() (*bytes.Buffer, error) { + var contents bytes.Buffer + tpl := template.Must(template.New("lima").Parse(ConfigTemplate)) - file, err := os.Create(i.ConfigFile) - if err != nil { - return fmt.Errorf("%v: %w", ConfigErr, err) + if err := tpl.Execute(&contents, i); err != nil { + return &contents, fmt.Errorf("%v: %w", ConfigErr, err) } - err = tpl.Execute(file, i) + return &contents, nil +} + +func (i *Instance) UpdateConfig() error { + contents, err := i.GenerateConfig() if err != nil { + return err + } + + if err := os.WriteFile(i.ConfigFile(), contents.Bytes(), 0666); err != nil { return fmt.Errorf("%v: %w", ConfigErr, err) } @@ -91,15 +105,6 @@ func (i *Instance) CreateInventoryFile() error { return nil } -func (i *Instance) DeleteConfig() error { - err := os.Remove(i.ConfigFile) - if err != nil { - return fmt.Errorf("Could not delete config file: %v", err) - } - - return nil -} - /* 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 diff --git a/pkg/lima/instance_test.go b/pkg/lima/instance_test.go index 8a00dbb0..40b55586 100644 --- a/pkg/lima/instance_test.go +++ b/pkg/lima/instance_test.go @@ -10,7 +10,7 @@ import ( "github.com/roots/trellis-cli/trellis" ) -func TestCreateConfig(t *testing.T) { +func TestGenerateConfig(t *testing.T) { defer trellis.LoadFixtureProject(t)() trellis := trellis.NewTrellis() if err := trellis.LoadProject(); err != nil { @@ -18,11 +18,9 @@ func TestCreateConfig(t *testing.T) { } dir := t.TempDir() - configFile := filepath.Join(dir, "lima.yaml") instance := &Instance{ - Dir: dir, - ConfigFile: configFile, + Dir: dir, Config: Config{ Images: []Image{ { @@ -40,12 +38,83 @@ func TestCreateConfig(t *testing.T) { Sites: trellis.Environments["development"].WordPressSites, } - err := instance.CreateConfig() + content, err := instance.GenerateConfig() if err != nil { t.Fatal(err) } - content, err := os.ReadFile(configFile) + absSitePath := filepath.Join(trellis.Path, "../site") + + expected := fmt.Sprintf(`vmType: "vz" +rosetta: + enabled: false +images: +- location: http://ubuntu.com/focal + arch: aarch64 + +mounts: +- location: %s + mountPoint: /srv/www/example.com/current + writable: true + +mountType: "virtiofs" +ssh: + forwardAgent: true +networks: +- vzNAT: true + +portForwards: +- guestPort: 80 + hostPort: 1234 + +containerd: + user: false +provision: +- mode: system + script: | + #!/bin/bash + echo "127.0.0.1 $(hostname)" >> /etc/hosts +`, absSitePath) + + if content.String() != expected { + t.Errorf("expected %s\ngot %s", expected, content.String()) + } +} + +func TestUpdateConfig(t *testing.T) { + defer trellis.LoadFixtureProject(t)() + trellis := trellis.NewTrellis() + if err := trellis.LoadProject(); err != nil { + t.Fatal(err) + } + + dir := t.TempDir() + + instance := &Instance{ + Dir: dir, + Config: Config{ + Images: []Image{ + { + Location: "http://ubuntu.com/focal", + Arch: "aarch64", + }, + }, + PortForwards: []PortForward{ + { + HostPort: 1234, + GuestPort: 80, + }, + }, + }, + Sites: trellis.Environments["development"].WordPressSites, + } + + err := instance.UpdateConfig() + if err != nil { + t.Fatal(err) + } + + content, err := os.ReadFile(instance.ConfigFile()) if err != nil { t.Fatal(err) diff --git a/pkg/lima/manager.go b/pkg/lima/manager.go index b437da4e..9d905835 100644 --- a/pkg/lima/manager.go +++ b/pkg/lima/manager.go @@ -80,20 +80,18 @@ func (m *Manager) GetInstance(name string) (Instance, bool) { func (m *Manager) CreateInstance(name string) error { instance := m.newInstance(name) - if err := instance.CreateConfig(); err != nil { - return err - } - - err := command.WithOptions( + cmd := command.WithOptions( command.WithTermOutput(), command.WithLogging(m.ui), - ).Cmd("limactl", []string{"start", "--tty=false", "--name=" + instance.Name, instance.ConfigFile}).Run() + ).Cmd("limactl", []string{"create", "--tty=false", "--name=" + instance.Name, "-"}) + configContents, err := instance.GenerateConfig() if err != nil { return err } - return postStart(m, instance) + cmd.Stdin = configContents + return cmd.Run() } func (m *Manager) DeleteInstance(name string) error { @@ -114,10 +112,6 @@ func (m *Manager) DeleteInstance(name string) error { return err } - if err := instance.DeleteConfig(); err != nil { - return err - } - return nil } else { return fmt.Errorf("Error: VM is running. Run `trellis vm stop` to stop it.") @@ -158,6 +152,10 @@ func (m *Manager) StartInstance(name string) error { return nil } + if err := instance.UpdateConfig(); err != nil { + return err + } + err := command.WithOptions( command.WithTermOutput(), command.WithLogging(m.ui), @@ -167,7 +165,24 @@ func (m *Manager) StartInstance(name string) error { return err } - return postStart(m, instance) + user, err := instance.getUsername() + if err != nil { + return fmt.Errorf("Could not get username: %v", err) + } + + instance.Username = string(user) + + // Hydrate instance with data from limactl that is only available after starting (mainly the forwarded SSH local port) + err = m.hydrateInstance(&instance) + if err != nil { + return err + } + + if err = m.addHosts(instance); err != nil { + return err + } + + return nil } func (m *Manager) StopInstance(name string) error { @@ -213,7 +228,6 @@ func (m *Manager) hydrateInstance(instance *Instance) error { } func (m *Manager) initInstance(instance *Instance) { - instance.ConfigFile = filepath.Join(m.ConfigPath, instance.Name+".yml") instance.InventoryFile = m.InventoryPath() instance.Sites = m.Sites } @@ -281,27 +295,6 @@ func (m *Manager) removeHosts(instance Instance) error { return m.HostsResolver.RemoveHosts(instance.Name) } -func postStart(manager *Manager, instance Instance) error { - user, err := instance.getUsername() - if err != nil { - return fmt.Errorf("Could not get username: %v", err) - } - - instance.Username = string(user) - - // Hydrate instance with data from limactl that is only available after starting (mainly the forwarded SSH local port) - err = manager.hydrateInstance(&instance) - if err != nil { - return err - } - - if err = manager.addHosts(instance); err != nil { - return err - } - - return nil -} - func getMacOSVersion() (string, error) { cmd := command.Cmd("sw_vers", []string{"-productVersion"}) b, err := cmd.Output() diff --git a/pkg/lima/manager_test.go b/pkg/lima/manager_test.go index 31371d8a..db4fc3e7 100644 --- a/pkg/lima/manager_test.go +++ b/pkg/lima/manager_test.go @@ -202,12 +202,11 @@ func TestCreateInstance(t *testing.T) { sshPort := 60720 username := "user1" ip := "192.168.64.2" - configFile := filepath.Join(manager.ConfigPath, instanceName+".yml") commands := []command.MockCommand{ { Command: "limactl", - Args: []string{"start", "--tty=false", "--name=" + instanceName, configFile}, + Args: []string{"create", "--tty=false", "--name=" + instanceName, "-"}, Output: ``, }, { @@ -218,7 +217,7 @@ func TestCreateInstance(t *testing.T) { { Command: "limactl", Args: []string{"ls", "--format=json"}, - Output: fmt.Sprintf(`{"name":"%s","status":"Running","dir":"/foo/test","vmType":"vz","arch":"aarch64","cpuType":"","cpus":4,"memory":4294967296,"disk":107374182400,"network":[{"vzNAT":true,"macAddress":"52:55:55:6f:d9:e3","interface":"lima0"}],"sshLocalPort":%d,"hostAgentPID":9390,"driverPID":9390}`, instanceName, sshPort), + Output: fmt.Sprintf(`{"name":"%s","status":"Stopped","dir":"/foo/test","vmType":"vz","arch":"aarch64","cpuType":"","cpus":4,"memory":4294967296,"disk":107374182400,"network":[{"vzNAT":true,"macAddress":"52:55:55:6f:d9:e3","interface":"lima0"}],"sshLocalPort":%d,"hostAgentPID":9390,"driverPID":9390}`, instanceName, sshPort), }, { Command: "limactl", @@ -236,6 +235,79 @@ func TestCreateInstance(t *testing.T) { t.Fatal(err) } + _, ok := manager.GetInstance(instanceName) + + if !ok { + t.Errorf("expected instance to be found") + } +} + +func TestStartInstance(t *testing.T) { + defer trellis.LoadFixtureProject(t)() + trellis := trellis.NewTrellis() + if err := trellis.LoadProject(); err != nil { + t.Fatal(err) + } + + t.Setenv("TRELLIS_BYPASS_LIMA_REQUIREMENTS", "1") + + ui := cli.NewMockUi() + manager, err := NewManager(trellis, ui) + if err != nil { + t.Fatal(err) + } + + tmpDir := t.TempDir() + + hostsStorage := make(map[string]string) + manager.HostsResolver = &MockHostsResolver{Hosts: hostsStorage} + + instanceName := "test" + sshPort := 60720 + username := "user1" + ip := "192.168.64.2" + + commands := []command.MockCommand{ + { + Command: "limactl", + Args: []string{"create", "--tty=false", "--name=" + instanceName, "-"}, + Output: ``, + }, + { + Command: "limactl", + Args: []string{"shell", instanceName, "whoami"}, + Output: username, + }, + { + Command: "limactl", + Args: []string{"ls", "--format=json"}, + Output: fmt.Sprintf(`{"name":"%s","status":"Stopped","dir":"%s","vmType":"vz","arch":"aarch64","cpuType":"","cpus":4,"memory":4294967296,"disk":107374182400,"network":[{"vzNAT":true,"macAddress":"52:55:55:6f:d9:e3","interface":"lima0"}],"sshLocalPort":%d,"hostAgentPID":9390,"driverPID":9390}`, instanceName, tmpDir, sshPort), + }, + { + Command: "limactl", + Args: []string{"shell", "--workdir", "/", instanceName, "ip", "route", "show", "dev", "lima0"}, + Output: fmt.Sprintf(`default via 192.168.64.1 proto dhcp src %s 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 +`, ip), + }, + { + Command: "limactl", + Args: []string{"start", instanceName}, + Output: ``, + }, + } + + defer command.MockExecCommands(t, commands)() + + if err = manager.CreateInstance(instanceName); err != nil { + t.Fatal(err) + } + + if err = manager.StartInstance(instanceName); err != nil { + t.Fatal(err) + } + inventoryContents, err := os.ReadFile(manager.InventoryPath()) if err != nil { t.Fatal(err)