Skip to content

Commit

Permalink
Merge pull request #346 from roots/add-vm-integration
Browse files Browse the repository at this point in the history
Add VM integration
  • Loading branch information
swalkinshaw authored Mar 3, 2023
2 parents c40fe80 + d3894d6 commit a0f6468
Show file tree
Hide file tree
Showing 36 changed files with 2,550 additions and 27 deletions.
28 changes: 26 additions & 2 deletions app_paths/app_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ const (
xdgDataHome = "XDG_DATA_HOME"
)

// Config path precedence: TRELLIS_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME.
// Config path precedence:
// 1. TRELLIS_CONFIG_DIR
// 2. XDG_CONFIG_HOME
// 3. AppData (windows only)
// 4. HOME
func ConfigDir() string {
var path string

Expand All @@ -37,7 +41,10 @@ func ConfigPath(path string) string {
return filepath.Join(ConfigDir(), path)
}

// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME.
// Cache path precedence:
// 1. XDG_CACHE_HOME
// 2. LocalAppData (windows only)
// 3. HOME
func CacheDir() string {
var path string
if a := os.Getenv(xdgCacheHome); a != "" {
Expand All @@ -50,3 +57,20 @@ func CacheDir() string {
}
return path
}

// Data path precedence:
// 1. XDG_DATA_HOME
// 2. LocalAppData (windows only)
// 3. HOME
func DataDir() string {
var path string
if a := os.Getenv(xdgDataHome); a != "" {
path = filepath.Join(a, "trellis")
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
path = filepath.Join(b, "Trellis CLI")
} else {
c, _ := os.UserHomeDir()
path = filepath.Join(c, ".local", "share", "trellis")
}
return path
}
40 changes: 33 additions & 7 deletions cli_config/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,32 @@ import (
"gopkg.in/yaml.v2"
)

type VmImage struct {
Location string `yaml:"location"`
Arch string `yaml:"arch"`
}

type VmConfig struct {
Manager string `yaml:"manager"`
HostsResolver string `yaml:"hosts_resolver"`
Images []VmImage `yaml:"images"`
Ubuntu string `yaml:"ubuntu"`
}

type Config struct {
AllowDevelopmentDeploys bool `yaml:"allow_development_deploys"`
AskVaultPass bool `yaml:"ask_vault_pass"`
CheckForUpdates bool `yaml:"check_for_updates"`
LoadPlugins bool `yaml:"load_plugins"`
Open map[string]string `yaml:"open"`
VirtualenvIntegration bool `yaml:"virtualenv_integration"`
Vm VmConfig `yaml:"vm"`
}

var (
ErrUnsupportedType = errors.New("Invalid env var config setting: value is an unsupported type.")
ErrCouldNotParse = errors.New("Invalid env var config setting: failed to parse value")
UnsupportedTypeErr = errors.New("Invalid env var config setting: value is an unsupported type.")
CouldNotParseErr = errors.New("Invalid env var config setting: failed to parse value")
InvalidConfigErr = errors.New("Invalid config file")
)

func NewConfig(defaultConfig Config) Config {
Expand All @@ -37,7 +51,19 @@ func (c *Config) LoadFile(path string) error {
}

if err := yaml.Unmarshal(configYaml, &c); err != nil {
return err
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.Ubuntu != "18.04" && c.Vm.Ubuntu != "20.04" && c.Vm.Ubuntu != "22.04" {
return fmt.Errorf("%w: unsupported value for `vm.ubuntu`. Must be one of: 18.04, 20.04, 22.04", InvalidConfigErr)
}

if c.Vm.HostsResolver != "hosts_file" {
return fmt.Errorf("%w: unsupported value for `vm.hosts_resolver`. Must be one of: hosts_file", InvalidConfigErr)
}

return nil
Expand Down Expand Up @@ -72,27 +98,27 @@ func (c *Config) LoadEnv(prefix string) error {
val, err := strconv.ParseBool(value)

if err != nil {
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as a boolean", ErrCouldNotParse, env, value)
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as a boolean", CouldNotParseErr, env, value)
}

structValue.SetBool(val)
case reflect.Int:
val, err := strconv.ParseInt(value, 10, 32)

if err != nil {
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as an integer", ErrCouldNotParse, env, value)
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as an integer", CouldNotParseErr, env, value)
}

structValue.SetInt(val)
case reflect.Float32:
val, err := strconv.ParseFloat(value, 32)
if err != nil {
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as a float", ErrCouldNotParse, env, value)
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as a float", CouldNotParseErr, env, value)
}

structValue.SetFloat(val)
default:
return fmt.Errorf("%w\n%s setting of type %s is unsupported.", ErrUnsupportedType, env, field.Type.String())
return fmt.Errorf("%w\n%s setting of type %s is unsupported.", UnsupportedTypeErr, env, field.Type.String())
}
}
}
Expand Down
22 changes: 20 additions & 2 deletions cmd/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ func (c *ProvisionCommand) Run(args []string) int {
var playbookFile string = "server.yml"

if environment == "development" {
os.Setenv("ANSIBLE_HOST_KEY_CHECKING", "false")
playbookFile = "dev.yml"
devInventoryFile := c.findDevInventory()

if _, err := os.Stat(filepath.Join(c.Trellis.Path, VagrantInventoryFilePath)); err == nil {
playbookArgs = append(playbookArgs, "--inventory-file", VagrantInventoryFilePath)
if devInventoryFile != "" {
playbookArgs = append(playbookArgs, "--inventory-file", devInventoryFile)
}
}

Expand Down Expand Up @@ -160,3 +162,19 @@ func (c *ProvisionCommand) AutocompleteFlags() complete.Flags {
"--verbose": complete.PredictNothing,
}
}

func (c *ProvisionCommand) findDevInventory() string {
manager, managerErr := newVmManager(c.Trellis, c.UI)
if managerErr == nil {
_, vmInventoryErr := os.Stat(manager.InventoryPath())
if vmInventoryErr == nil {
return manager.InventoryPath()
}
}

if _, vagrantInventoryErr := os.Stat(filepath.Join(c.Trellis.Path, VagrantInventoryFilePath)); vagrantInventoryErr == nil {
return VagrantInventoryFilePath
}

return ""
}
1 change: 1 addition & 0 deletions cmd/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func TestProvisionRunValidations(t *testing.T) {
func TestProvisionRun(t *testing.T) {
defer trellis.LoadFixtureProject(t)()
trellis := trellis.NewTrellis()
trellis.CliConfig.Vm.Manager = "mock"

cases := []struct {
name string
Expand Down
29 changes: 29 additions & 0 deletions cmd/vm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"fmt"
"runtime"

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

func newVmManager(trellis *trellis.Trellis, ui cli.Ui) (manager vm.Manager, err error) {
switch trellis.CliConfig.Vm.Manager {
case "auto":
switch runtime.GOOS {
case "darwin":
return lima.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 "mock":
return vm.NewMockManager(trellis, ui)
}

return nil, fmt.Errorf("VM manager not found")
}
118 changes: 118 additions & 0 deletions cmd/vm_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cmd

import (
"flag"
"strings"

"github.com/manifoldco/promptui"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/roots/trellis-cli/trellis"
)

type VmDeleteCommand struct {
UI cli.Ui
Trellis *trellis.Trellis
flags *flag.FlagSet
force bool
}

func NewVmDeleteCommand(ui cli.Ui, trellis *trellis.Trellis) *VmDeleteCommand {
c := &VmDeleteCommand{UI: ui, Trellis: trellis}
c.init()
return c
}

func (c *VmDeleteCommand) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.Usage = func() { c.UI.Info(c.Help()) }
c.flags.BoolVar(&c.force, "force", false, "Delete VM without confirmation.")
}

func (c *VmDeleteCommand) Run(args []string) int {
if err := c.Trellis.LoadProject(); err != nil {
c.UI.Error(err.Error())
return 1
}

c.Trellis.CheckVirtualenv(c.UI)

if err := c.flags.Parse(args); err != nil {
return 1
}

args = c.flags.Args()

commandArgumentValidator := &CommandArgumentValidator{required: 0, optional: 0}
commandArgumentErr := commandArgumentValidator.validate(args)
if commandArgumentErr != nil {
c.UI.Error(commandArgumentErr.Error())
c.UI.Output(c.Help())
return 1
}

siteName, err := c.Trellis.FindSiteNameFromEnvironment("development", "")
if err != nil {
c.UI.Error(err.Error())
return 1
}

manager, err := newVmManager(c.Trellis, c.UI)
if err != nil {
c.UI.Error("Error: " + err.Error())
return 1
}

if c.force || c.confirmDeletion() {
if err := manager.DeleteInstance(siteName); err != nil {
c.UI.Error("Error: " + err.Error())
return 1
}
}

return 0
}

func (c *VmDeleteCommand) Synopsis() string {
return "Deletes the development virtual machine."
}

func (c *VmDeleteCommand) Help() string {
helpText := `
Usage: trellis vm delete [options]
Deletes the development virtual machine.
VMs must be in a stopped state before they can be deleted.
Delete without prompting for confirmation:
$ trellis vm delete --force
Options:
--force Delete VM without confirmation.
-h, --help Show this help
`

return strings.TrimSpace(helpText)
}

func (c *VmDeleteCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"--force": complete.PredictNothing,
}
}

func (c *VmDeleteCommand) confirmDeletion() bool {
prompt := promptui.Prompt{
Label: "Delete virtual machine",
IsConfirm: true,
}

_, err := prompt.Run()

if err != nil {
c.UI.Info("Aborted. Not deleting virtual machine.")
return false
}

return true
}
56 changes: 56 additions & 0 deletions cmd/vm_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cmd

import (
"strings"
"testing"

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

func TestVmDeleteRunValidations(t *testing.T) {
defer trellis.LoadFixtureProject(t)()

cases := []struct {
name string
projectDetected bool
args []string
out string
code int
}{
{
"no_project",
false,
nil,
"No Trellis project detected",
1,
},
{
"too_many_args",
true,
[]string{"foo"},
"Error: too many arguments",
1,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi()
trellis := trellis.NewMockTrellis(tc.projectDetected)
vmDeleteCommand := NewVmDeleteCommand(ui, trellis)

code := vmDeleteCommand.Run(tc.args)

if code != tc.code {
t.Errorf("expected code %d to be %d", code, tc.code)
}

combined := ui.OutputWriter.String() + ui.ErrorWriter.String()

if !strings.Contains(combined, tc.out) {
t.Errorf("expected output %q to contain %q", combined, tc.out)
}
})
}
}
Loading

0 comments on commit a0f6468

Please sign in to comment.