Skip to content
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
2 changes: 1 addition & 1 deletion cli_config/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (c *Config) LoadFile(path string) error {
return fmt.Errorf("%w: %s", InvalidConfigErr, err)
}

if c.Vm.Manager != "" && c.Vm.Manager != "lima" && c.Vm.Manager != "auto" && c.Vm.Manager != "mock" {
if c.Vm.Manager != "" && 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", InvalidConfigErr)
}

Expand Down
5 changes: 5 additions & 0 deletions cmd/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/hashicorp/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 @@ -17,11 +18,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
20 changes: 20 additions & 0 deletions pkg/lxd/files/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
config:
raw.idmap: |
uid {{ .Uid }} 1000
gid {{ .Gid }} 1000
user.vendor-data: |
#cloud-config
manage_etc_hosts: localhost
users:
- name: {{ .Username }}
groups: sudo
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- {{ .SshPublicKey }}
devices:
{{ range $name, $device := .Devices }}
{{ $name }}:
type: disk
source: {{ $device.Source }}
path: {{ $device.Dest }}
{{ end }}
7 changes: 7 additions & 0 deletions pkg/lxd/files/inventory.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
default ansible_host={{ .IP }} ansible_user={{ .Username }} ansible_ssh_common_args='-o StrictHostKeyChecking=no'

[development]
default

[web]
default
109 changes: 109 additions & 0 deletions pkg/lxd/instance.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 Instance struct {
ConfigFile string
InventoryFile string
Sites map[string]*trellis.Site
Name string `json:"name"`
State State `json:"state"`
Username string `json:"username,omitempty"`
Uid int
Gid int
SshPublicKey string
Devices map[string]Device
}

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

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

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

return nil
}

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

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

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

return nil
}

func (i *Instance) IP() (ip string, err error) {
network, ok := i.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 (i *Instance) Running() bool {
return i.State.Status == "Running"
}

func (i *Instance) Stopped() bool {
return i.State.Status == "Stopped"
}
78 changes: 78 additions & 0 deletions pkg/lxd/instance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package lxd

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

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

func TestCreateInventoryFile(t *testing.T) {
dir := t.TempDir()

expectedIP := "1.2.3.4"

instance := &Instance{
InventoryFile: filepath.Join(dir, "inventory"),
Username: "dev",
State: State{
Network: map[string]Network{
"eth0": {
Addresses: []NetworkAddress{{Address: expectedIP, Family: "inet"}}},
},
},
}

err := instance.CreateInventoryFile()
if err != nil {
t.Fatal(err)
}

content, err := os.ReadFile(instance.InventoryFile)

if err != nil {
t.Fatal(err)
}

expected := fmt.Sprintf(`default ansible_host=%s ansible_user=dev ansible_ssh_common_args='-o StrictHostKeyChecking=no'

[development]
default

[web]
default
`, expectedIP)

if string(content) != expected {
t.Errorf("expected %s\ngot %s", expected, string(content))
}
}

func TestIP(t *testing.T) {
expectedIP := "10.99.30.5"

instance := &Instance{
Name: "test",
State: State{
Network: map[string]Network{
"eth0": {
Addresses: []NetworkAddress{{Address: expectedIP, Family: "inet"}}},
},
},
}

ip, err := instance.IP()
if err != nil {
t.Fatal(err)
}

if ip != expectedIP {
t.Errorf("expected %s\ngot %s", expectedIP, ip)
}
}

func TestCommandHelperProcess(t *testing.T) {
command.CommandHelperProcess(t)
}
36 changes: 36 additions & 0 deletions pkg/lxd/lxd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package lxd

import (
"fmt"
"os/exec"
"regexp"

"github.com/mcuadros/go-version"
"github.com/roots/trellis-cli/command"
)

const (
VersionRequired = ">= 0.14.0"
)

func Installed() error {
if _, err := exec.LookPath("lxc"); err != nil {
return fmt.Errorf("LXD is not installed.")
}

output, err := command.Cmd("lxc", []string{"-v"}).Output()
if err != nil {
return fmt.Errorf("Could get determine the version of LXD.")
}

re := regexp.MustCompile(`.*([0-9]+\.[0-9]+\.[0-9]+(-alpha|beta)?)`)
v := re.FindStringSubmatch(string(output))
constraint := version.NewConstrainGroupFromString(VersionRequired)
matched := constraint.Match(v[1])

if !matched {
return fmt.Errorf("LXD version %s does not satisfy required version (%s).", v[1], VersionRequired)
}

return nil
}
Loading
Loading