Skip to content

Add LXD VM support #363

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions cli_config/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ func (c *Config) LoadFile(path string) error {
return fmt.Errorf("%w: %s", InvalidConfigErr, err)
}

if c.Vm.Manager != "lima" && c.Vm.Manager != "auto" && c.Vm.Manager != "mock" {
return fmt.Errorf("%w: unsupported value for `vm.manager`. Must be one of: auto, lima", InvalidConfigErr)
if c.Vm.Manager != "lima" && c.Vm.Manager != "auto" && c.Vm.Manager != "mock" && c.Vm.Manager != "lxd" {
return fmt.Errorf("%w: unsupported value for `vm_manager`. Must be one of: auto, lima, lxd", InvalidConfigErr)
}

if c.Vm.Ubuntu != "18.04" && c.Vm.Ubuntu != "20.04" && c.Vm.Ubuntu != "22.04" {
Expand Down
5 changes: 5 additions & 0 deletions cmd/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/mitchellh/cli"
"github.com/roots/trellis-cli/pkg/lima"
"github.com/roots/trellis-cli/pkg/lxd"
"github.com/roots/trellis-cli/pkg/vm"
"github.com/roots/trellis-cli/trellis"
)
Expand All @@ -16,11 +17,15 @@ func newVmManager(trellis *trellis.Trellis, ui cli.Ui) (manager vm.Manager, err
switch runtime.GOOS {
case "darwin":
return lima.NewManager(trellis, ui)
case "linux":
return lxd.NewManager(trellis, ui)
default:
return nil, fmt.Errorf("No VM managers are supported on %s yet.", runtime.GOOS)
}
case "lima":
return lima.NewManager(trellis, ui)
case "lxd":
return lxd.NewManager(trellis, ui)
case "mock":
return vm.NewMockManager(trellis, ui)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/vm_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (c *VmDeleteCommand) Run(args []string) int {
}

if c.force || c.confirmDeletion() {
if err := manager.DeleteInstance(siteName); err != nil {
if err := manager.DeleteVM(siteName); err != nil {
c.UI.Error("Error: " + err.Error())
return 1
}
Expand Down
10 changes: 5 additions & 5 deletions cmd/vm_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ func (c *VmStartCommand) Run(args []string) int {
return 1
}

err = manager.StartInstance(siteName)
err = manager.StartVM(siteName)
if err == nil {
c.printInstanceInfo()
c.printVMInfo()
return 0
}

Expand All @@ -74,7 +74,7 @@ func (c *VmStartCommand) Run(args []string) int {
}

// VM doesn't exist yet, create it
if err = manager.CreateInstance(siteName); err != nil {
if err = manager.CreateVM(siteName); err != nil {
c.UI.Error("Error creating VM.")
c.UI.Error(err.Error())
return 1
Expand All @@ -86,7 +86,7 @@ func (c *VmStartCommand) Run(args []string) int {
code := provisionCmd.Run([]string{"development"})

if code == 0 {
c.printInstanceInfo()
c.printVMInfo()
}

return code
Expand All @@ -113,7 +113,7 @@ Options:
return strings.TrimSpace(helpText)
}

func (c *VmStartCommand) printInstanceInfo() {
func (c *VmStartCommand) printVMInfo() {
c.UI.Info(`
Your Trellis VM is ready to use!

Expand Down
2 changes: 1 addition & 1 deletion cmd/vm_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *VmStopCommand) Run(args []string) int {
return 1
}

if err := manager.StopInstance(siteName); err != nil {
if err := manager.StopVM(siteName); err != nil {
c.UI.Error(err.Error())
return 1
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/lima/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var inventoryTemplate string

var (
ConfigErr = errors.New("Could not write Lima config file")
IpErr = errors.New("Could not determine IP address for VM instance")
IpErr = errors.New("Could not determine IP address for VM")
)

type PortForward struct {
Expand Down
34 changes: 17 additions & 17 deletions pkg/lima/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ func (m *Manager) InventoryPath() string {
return filepath.Join(m.ConfigPath, "inventory")
}

func (m *Manager) GetInstance(name string) (Instance, bool) {
func (m *Manager) GetVM(name string) (Instance, bool) {
instances := m.instances()
instance, ok := instances[name]

return instance, ok
}

func (m *Manager) CreateInstance(name string) error {
instance := m.newInstance(name)
func (m *Manager) CreateVM(name string) error {
instance := m.newVM(name)

if err := instance.CreateConfig(); err != nil {
return err
Expand All @@ -96,8 +96,8 @@ func (m *Manager) CreateInstance(name string) error {
return postStart(m, instance)
}

func (m *Manager) DeleteInstance(name string) error {
instance, ok := m.GetInstance(name)
func (m *Manager) DeleteVM(name string) error {
instance, ok := m.GetVM(name)

if !ok {
m.ui.Info("VM does not exist for this project. Run `trellis vm start` to create it.")
Expand All @@ -116,7 +116,7 @@ func (m *Manager) DeleteInstance(name string) error {

// TODO: set working dir to site path?
func (m *Manager) OpenShell(name string, commandArgs []string) error {
instance, ok := m.GetInstance(name)
instance, ok := m.GetVM(name)

if !ok {
m.ui.Info("VM does not exist for this project. Run `trellis vm start` to create it.")
Expand All @@ -137,8 +137,8 @@ func (m *Manager) OpenShell(name string, commandArgs []string) error {
).Cmd("limactl", args).Run()
}

func (m *Manager) StartInstance(name string) error {
instance, ok := m.GetInstance(name)
func (m *Manager) StartVM(name string) error {
instance, ok := m.GetVM(name)

if !ok {
return vm.VmNotFoundErr
Expand All @@ -161,8 +161,8 @@ func (m *Manager) StartInstance(name string) error {
return postStart(m, instance)
}

func (m *Manager) StopInstance(name string) error {
instance, ok := m.GetInstance(name)
func (m *Manager) StopVM(name string) error {
instance, ok := m.GetVM(name)

if !ok {
m.ui.Info("VM does not exist for this project. Run `trellis vm start` to create it.")
Expand Down Expand Up @@ -190,8 +190,8 @@ func (m *Manager) StopInstance(name string) error {
return nil
}

func (m *Manager) hydrateInstance(instance *Instance) error {
i, _ := m.GetInstance(instance.Name)
func (m *Manager) hydrateVM(instance *Instance) error {
i, _ := m.GetVM(instance.Name)
tmpJson, err := json.Marshal(i)
if err != nil {
return fmt.Errorf("Could not marshal instance: %v\nThis is a trellis-cli bug.", err)
Expand All @@ -203,15 +203,15 @@ func (m *Manager) hydrateInstance(instance *Instance) error {
return nil
}

func (m *Manager) initInstance(instance *Instance) {
func (m *Manager) initVM(instance *Instance) {
instance.ConfigFile = filepath.Join(m.ConfigPath, instance.Name+".yml")
instance.InventoryFile = m.InventoryPath()
instance.Sites = m.Sites
}

func (m *Manager) newInstance(name string) Instance {
func (m *Manager) newVM(name string) Instance {
instance := Instance{Name: name}
m.initInstance(&instance)
m.initVM(&instance)

images := []Image{}

Expand Down Expand Up @@ -261,7 +261,7 @@ func (m *Manager) instances() (instances map[string]Instance) {
for _, line := range bytes.Split(output, []byte("\n")) {
instance := &Instance{}
json.Unmarshal([]byte(line), instance)
m.initInstance(instance)
m.initVM(instance)
instances[instance.Name] = *instance
}

Expand All @@ -281,7 +281,7 @@ func postStart(manager *Manager, instance Instance) error {
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)
err = manager.hydrateVM(&instance)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/lima/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestInitInstance(t *testing.T) {
}

instance := Instance{Name: "test"}
manager.initInstance(&instance)
manager.initVM(&instance)

if instance.Name != "test" {
t.Errorf("expected instance name to be %q, got %q", "test", instance.Name)
Expand All @@ -120,7 +120,7 @@ func TestNewInstanceUbuntuVersion(t *testing.T) {
t.Fatal(err)
}

instance := manager.newInstance("test")
instance := manager.newVM("test")

if instance.Name != "test" {
t.Errorf("expected instance name to be %q, got %q", "test", instance.Name)
Expand Down Expand Up @@ -232,7 +232,7 @@ func TestCreateInstance(t *testing.T) {

defer command.MockExecCommands(t, commands)()

if err = manager.CreateInstance(instanceName); err != nil {
if err = manager.CreateVM(instanceName); err != nil {
t.Fatal(err)
}

Expand Down
109 changes: 109 additions & 0 deletions pkg/lxd/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package lxd

import (
_ "embed"
"errors"
"fmt"
"os"
"text/template"

"github.com/roots/trellis-cli/trellis"
)

//go:embed files/config.yml
var ConfigTemplate string

//go:embed files/inventory.txt
var inventoryTemplate string

var (
ConfigErr = errors.New("Could not write LXD config file")
IpErr = errors.New("Could not determine IP address for VM")
)

type Device struct {
Source string
Dest string
}

type NetworkAddress struct {
Family string `json:"family"`
Address string `json:"address"`
}

type Network struct {
Addresses []NetworkAddress `json:"addresses"`
}

type State struct {
Status string `json:"status"`
Network map[string]Network `json:"network"`
}

type Container struct {
ConfigFile string
InventoryFile string
Sites map[string]*trellis.Site
Name string `json:"name"`
State State `json:"state"`
Username string `json:"username,omitempty"`
Uid string
Gid string
SshPublicKey string
Devices map[string]Device
}

func (c *Container) CreateConfig() error {
tpl := template.Must(template.New("lxc").Parse(ConfigTemplate))

file, err := os.Create(c.ConfigFile)
if err != nil {
return fmt.Errorf("%v: %w", ConfigErr, err)
}

err = tpl.Execute(file, c)
if err != nil {
return fmt.Errorf("%v: %w", ConfigErr, err)
}

return nil
}

func (c *Container) CreateInventoryFile() error {
tpl := template.Must(template.New("lxd").Parse(inventoryTemplate))

file, err := os.Create(c.InventoryFile)
if err != nil {
return fmt.Errorf("Could not create Ansible inventory file: %v", err)
}

err = tpl.Execute(file, c)
if err != nil {
return fmt.Errorf("Could not template Ansible inventory file: %v", err)
}

return nil
}

func (c *Container) IP() (ip string, err error) {
network, ok := c.State.Network["eth0"]
if !ok {
return "", fmt.Errorf("%v: eth0 network not found", IpErr)
}

for _, address := range network.Addresses {
if address.Family == "inet" && address.Address != "" {
return address.Address, nil
}
}

return "", fmt.Errorf("%v: inet address family not found", IpErr)
}

func (c *Container) Running() bool {
return c.State.Status == "Running"
}

func (c *Container) Stopped() bool {
return c.State.Status == "Stopped"
}
Loading