Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persistent VM Naming #523

Closed
wants to merge 2 commits into from
Closed
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
8 changes: 4 additions & 4 deletions cli_config/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ type VmImage struct {
}

type VmConfig struct {
Manager string `yaml:"manager"`
HostsResolver string `yaml:"hosts_resolver"`
Images []VmImage `yaml:"images"`
Ubuntu string `yaml:"ubuntu"`
Manager string `yaml:"manager"`
HostsResolver string `yaml:"hosts_resolver"`
Ubuntu string `yaml:"ubuntu"`
InstanceName string `yaml:"instance_name"` // Custom name for the Lima VM instance
}

type Config struct {
Expand Down
8 changes: 7 additions & 1 deletion cmd/vm_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"flag"
"strings"
"os"
"path/filepath"

"github.com/manifoldco/promptui"
"github.com/mitchellh/cli"
Expand Down Expand Up @@ -67,7 +69,11 @@ func (c *VmDeleteCommand) Run(args []string) int {
if err := manager.DeleteInstance(siteName); err != nil {
c.UI.Error("Error: " + err.Error())
return 1
}
}

// Remove instance file if it exists
instancePath := filepath.Join(c.Trellis.ConfigPath(), "lima", "instance")
os.Remove(instancePath) // Ignore errors as file may not exist
}

return 0
Expand Down
49 changes: 49 additions & 0 deletions cmd/vm_delete_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -54,3 +56,50 @@ func TestVmDeleteRunValidations(t *testing.T) {
})
}
}

func TestVmDeleteRemovesInstanceFile(t *testing.T) {
cleanup := trellis.LoadFixtureProject(t)
defer cleanup()

// Setup test environment
ui := cli.NewMockUi()
mockTrellis := trellis.NewTrellis()
mockTrellis.LoadProject()

// Create the lima directory and instance file
limaDir := filepath.Join(mockTrellis.ConfigPath(), "lima")
os.MkdirAll(limaDir, 0755)
instancePath := filepath.Join(limaDir, "instance")
os.WriteFile(instancePath, []byte("example.com"), 0644)

// Verify file exists before test
if _, err := os.Stat(instancePath); os.IsNotExist(err) {
t.Fatalf("failed to create test instance file")
}

// Create command
vmDeleteCommand := NewVmDeleteCommand(ui, mockTrellis)
vmDeleteCommand.force = true // Skip confirmation prompt

// Replace VM manager with mock
originalNewVmManager := newVmManager
mockManager := &MockVmManager{}
newVmManager = func(t *trellis.Trellis, ui cli.Ui) (vm.Manager, error) {
return mockManager, nil
}
defer func() { newVmManager = originalNewVmManager }()

// Run command
code := vmDeleteCommand.Run([]string{})

// Check command succeeded
if code != 0 {
t.Errorf("expected exit code 0, got %d", code)
}

// Check instance file was removed
_, err := os.Stat(instancePath)
if !os.IsNotExist(err) {
t.Error("expected instance file to be deleted")
}
}
7 changes: 6 additions & 1 deletion cmd/vm_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (c *VmStartCommand) Run(args []string) int {
return 0
}

if !errors.Is(err, vm.VmNotFoundErr) {
if (!errors.Is(err, vm.VmNotFoundErr)) {
c.UI.Error("Error starting VM.")
c.UI.Error(err.Error())
return 1
Expand All @@ -80,6 +80,11 @@ func (c *VmStartCommand) Run(args []string) int {
return 1
}

// Save the instance name for future reference
if err = c.Trellis.SaveVMInstanceName(siteName); err != nil {
c.UI.Warn("Warning: Failed to save VM instance name. VM was created successfully, but future commands may not recognize it.")
}

if err = manager.StartInstance(siteName); err != nil {
c.UI.Error("Error starting VM.")
c.UI.Error(err.Error())
Expand Down
92 changes: 92 additions & 0 deletions cmd/vm_start_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package cmd

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/mitchellh/cli"
"github.com/roots/trellis-cli/pkg/vm"
"github.com/roots/trellis-cli/trellis"
)

Expand Down Expand Up @@ -54,3 +57,92 @@ func TestVmStartRunValidations(t *testing.T) {
})
}
}

// MockVmManager for testing
type MockVmManager struct {
createCalled bool
startCalled bool
siteName string
}

func (m *MockVmManager) CreateInstance(name string) error {
m.createCalled = true
m.siteName = name
return nil
}

func (m *MockVmManager) StartInstance(name string) error {
m.startCalled = true
m.siteName = name
// First call returns VmNotFoundErr to trigger creation
if !m.createCalled {
return vm.VmNotFoundErr
}
return nil
}

func (m *MockVmManager) StopInstance(name string) error {
return nil
}

func (m *MockVmManager) DeleteInstance(name string) error {
return nil
}

func TestVmStartSavesInstanceName(t *testing.T) {
cleanup := trellis.LoadFixtureProject(t)
defer cleanup()

// Setup test environment
ui := cli.NewMockUi()
mockTrellis := trellis.NewTrellis()
mockTrellis.LoadProject()

// Create command
vmStartCommand := NewVmStartCommand(ui, mockTrellis)

// Replace VM manager with mock
originalNewVmManager := newVmManager
mockManager := &MockVmManager{}
newVmManager = func(t *trellis.Trellis, ui cli.Ui) (vm.Manager, error) {
return mockManager, nil
}
defer func() { newVmManager = originalNewVmManager }()

// Mock provision command to return success
originalNewProvisionCommand := NewProvisionCommand
NewProvisionCommand = func(ui cli.Ui, trellis *trellis.Trellis) *ProvisionCommand {
cmd := &ProvisionCommand{UI: ui, Trellis: trellis}
cmd.Run = func(args []string) int {
return 0
}
return cmd
}
defer func() { NewProvisionCommand = originalNewProvisionCommand }()

// Run command
code := vmStartCommand.Run([]string{})

// Check VM was created and started
if code != 0 {
t.Errorf("expected exit code 0, got %d", code)
}
if !mockManager.createCalled {
t.Error("expected CreateInstance to be called")
}
if !mockManager.startCalled {
t.Error("expected StartInstance to be called")
}

// Check instance file was created
instancePath := filepath.Join(mockTrellis.ConfigPath(), "lima", "instance")
data, err := os.ReadFile(instancePath)
if err != nil {
t.Errorf("expected instance file to exist: %v", err)
}

instanceName := strings.TrimSpace(string(data))
if instanceName != mockManager.siteName {
t.Errorf("expected instance name %q, got %q", mockManager.siteName, instanceName)
}
}
28 changes: 20 additions & 8 deletions trellis/trellis.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,16 +263,28 @@ func (t *Trellis) FindSiteNameFromEnvironment(environment string, siteNameArg st
return "", fmt.Errorf("Error: %s is not a valid site. Valid options are %s", siteNameArg, siteNames)
}

func (t *Trellis) MainSiteFromEnvironment(environment string) (string, *Site, error) {
sites := t.SiteNamesFromEnvironment(environment)

if len(sites) == 0 {
return "", nil, fmt.Errorf("Error: No sites found in %s environment", environment)
func (t *Trellis) MainSiteFromEnvironment(env string) (string, *Site, error) {
if _, ok := t.Environments[env]; !ok {
return "", nil, fmt.Errorf("environment %s not found", env)
}

name := sites[0]

return name, t.Environments[environment].WordPressSites[name], nil
// Get the instance name using our new priority system
siteName, err := t.GetVMInstanceName()
if err != nil || siteName == "" {
// Fall back to using the first site name if there's an error or empty result
siteNames := t.SiteNamesFromEnvironment(env)
if len(siteNames) == 0 {
return "", nil, fmt.Errorf("no sites found in environment %s", env)
}
siteName = siteNames[0]
}

site, ok := t.Environments[env].WordPressSites[siteName]
if !ok {
return "", nil, fmt.Errorf("site %s not found in environment %s", siteName, env)
}

return siteName, site, nil
}

func (t *Trellis) getDefaultSiteNameFromEnvironment(environment string) (siteName string, err error) {
Expand Down
83 changes: 83 additions & 0 deletions trellis/trellis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,86 @@
t.Errorf("expected load project to load project CLI config file")
}
}

func TestGetVMInstanceName(t *testing.T) {

Check failure on line 448 in trellis/trellis_test.go

View workflow job for this annotation

GitHub Actions / test

other declaration of TestGetVMInstanceName
defer LoadFixtureProject(t)()

tp := NewTrellis()
err := tp.LoadProject()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Test case 1: No instance file and no config setting - should use first site
instanceName, err := tp.GetVMInstanceName()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedName := tp.SiteNamesFromEnvironment("development")[0]
if instanceName != expectedName {
t.Errorf("expected instance name %q, got %q", expectedName, instanceName)
}

// Test case 2: With config setting - should use config value
tp.CliConfig.Vm.InstanceName = "configured-name"
instanceName, err = tp.GetVMInstanceName()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if instanceName != "configured-name" {
t.Errorf("expected instance name %q, got %q", "configured-name", instanceName)
}

// Test case 3: With instance file - should use file value (highest priority)
// Create the instance file
limaDir := filepath.Join(tp.ConfigPath(), "lima")
if err := os.MkdirAll(limaDir, 0755); err != nil {
t.Fatalf("failed to create lima directory: %v", err)
}
instancePath := filepath.Join(limaDir, "instance")
if err := os.WriteFile(instancePath, []byte("file-instance-name"), 0644); err != nil {
t.Fatalf("failed to write instance file: %v", err)
}

instanceName, err = tp.GetVMInstanceName()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if instanceName != "file-instance-name" {
t.Errorf("expected instance name %q, got %q", "file-instance-name", instanceName)
}

// Clean up
os.Remove(instancePath)
tp.CliConfig.Vm.InstanceName = ""
}

func TestSaveVMInstanceName(t *testing.T) {

Check failure on line 501 in trellis/trellis_test.go

View workflow job for this annotation

GitHub Actions / test

other declaration of TestSaveVMInstanceName
defer LoadFixtureProject(t)()

tp := NewTrellis()
err := tp.LoadProject()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Test saving the instance name
err = tp.SaveVMInstanceName("test-instance")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Verify file was created and contains correct content
instancePath := filepath.Join(tp.ConfigPath(), "lima", "instance")
data, err := os.ReadFile(instancePath)
if err != nil {
t.Fatalf("failed to read instance file: %v", err)
}

if string(data) != "test-instance" {
t.Errorf("expected instance file to contain %q, got %q", "test-instance", string(data))
}

// Clean up
os.Remove(instancePath)
}
Loading
Loading