Skip to content

Commit a0f6468

Browse files
authored
Merge pull request #346 from roots/add-vm-integration
Add VM integration
2 parents c40fe80 + d3894d6 commit a0f6468

36 files changed

+2550
-27
lines changed

app_paths/app_paths.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ const (
1515
xdgDataHome = "XDG_DATA_HOME"
1616
)
1717

18-
// Config path precedence: TRELLIS_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME.
18+
// Config path precedence:
19+
// 1. TRELLIS_CONFIG_DIR
20+
// 2. XDG_CONFIG_HOME
21+
// 3. AppData (windows only)
22+
// 4. HOME
1923
func ConfigDir() string {
2024
var path string
2125

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

40-
// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME.
44+
// Cache path precedence:
45+
// 1. XDG_CACHE_HOME
46+
// 2. LocalAppData (windows only)
47+
// 3. HOME
4148
func CacheDir() string {
4249
var path string
4350
if a := os.Getenv(xdgCacheHome); a != "" {
@@ -50,3 +57,20 @@ func CacheDir() string {
5057
}
5158
return path
5259
}
60+
61+
// Data path precedence:
62+
// 1. XDG_DATA_HOME
63+
// 2. LocalAppData (windows only)
64+
// 3. HOME
65+
func DataDir() string {
66+
var path string
67+
if a := os.Getenv(xdgDataHome); a != "" {
68+
path = filepath.Join(a, "trellis")
69+
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
70+
path = filepath.Join(b, "Trellis CLI")
71+
} else {
72+
c, _ := os.UserHomeDir()
73+
path = filepath.Join(c, ".local", "share", "trellis")
74+
}
75+
return path
76+
}

cli_config/cli_config.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,32 @@ import (
1111
"gopkg.in/yaml.v2"
1212
)
1313

14+
type VmImage struct {
15+
Location string `yaml:"location"`
16+
Arch string `yaml:"arch"`
17+
}
18+
19+
type VmConfig struct {
20+
Manager string `yaml:"manager"`
21+
HostsResolver string `yaml:"hosts_resolver"`
22+
Images []VmImage `yaml:"images"`
23+
Ubuntu string `yaml:"ubuntu"`
24+
}
25+
1426
type Config struct {
1527
AllowDevelopmentDeploys bool `yaml:"allow_development_deploys"`
1628
AskVaultPass bool `yaml:"ask_vault_pass"`
1729
CheckForUpdates bool `yaml:"check_for_updates"`
1830
LoadPlugins bool `yaml:"load_plugins"`
1931
Open map[string]string `yaml:"open"`
2032
VirtualenvIntegration bool `yaml:"virtualenv_integration"`
33+
Vm VmConfig `yaml:"vm"`
2134
}
2235

2336
var (
24-
ErrUnsupportedType = errors.New("Invalid env var config setting: value is an unsupported type.")
25-
ErrCouldNotParse = errors.New("Invalid env var config setting: failed to parse value")
37+
UnsupportedTypeErr = errors.New("Invalid env var config setting: value is an unsupported type.")
38+
CouldNotParseErr = errors.New("Invalid env var config setting: failed to parse value")
39+
InvalidConfigErr = errors.New("Invalid config file")
2640
)
2741

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

3953
if err := yaml.Unmarshal(configYaml, &c); err != nil {
40-
return err
54+
return fmt.Errorf("%w: %s", InvalidConfigErr, err)
55+
}
56+
57+
if c.Vm.Manager != "lima" && c.Vm.Manager != "auto" && c.Vm.Manager != "mock" {
58+
return fmt.Errorf("%w: unsupported value for `vm.manager`. Must be one of: auto, lima", InvalidConfigErr)
59+
}
60+
61+
if c.Vm.Ubuntu != "18.04" && c.Vm.Ubuntu != "20.04" && c.Vm.Ubuntu != "22.04" {
62+
return fmt.Errorf("%w: unsupported value for `vm.ubuntu`. Must be one of: 18.04, 20.04, 22.04", InvalidConfigErr)
63+
}
64+
65+
if c.Vm.HostsResolver != "hosts_file" {
66+
return fmt.Errorf("%w: unsupported value for `vm.hosts_resolver`. Must be one of: hosts_file", InvalidConfigErr)
4167
}
4268

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

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

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

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

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

93119
structValue.SetFloat(val)
94120
default:
95-
return fmt.Errorf("%w\n%s setting of type %s is unsupported.", ErrUnsupportedType, env, field.Type.String())
121+
return fmt.Errorf("%w\n%s setting of type %s is unsupported.", UnsupportedTypeErr, env, field.Type.String())
96122
}
97123
}
98124
}

cmd/provision.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ func (c *ProvisionCommand) Run(args []string) int {
8787
var playbookFile string = "server.yml"
8888

8989
if environment == "development" {
90+
os.Setenv("ANSIBLE_HOST_KEY_CHECKING", "false")
9091
playbookFile = "dev.yml"
92+
devInventoryFile := c.findDevInventory()
9193

92-
if _, err := os.Stat(filepath.Join(c.Trellis.Path, VagrantInventoryFilePath)); err == nil {
93-
playbookArgs = append(playbookArgs, "--inventory-file", VagrantInventoryFilePath)
94+
if devInventoryFile != "" {
95+
playbookArgs = append(playbookArgs, "--inventory-file", devInventoryFile)
9496
}
9597
}
9698

@@ -160,3 +162,19 @@ func (c *ProvisionCommand) AutocompleteFlags() complete.Flags {
160162
"--verbose": complete.PredictNothing,
161163
}
162164
}
165+
166+
func (c *ProvisionCommand) findDevInventory() string {
167+
manager, managerErr := newVmManager(c.Trellis, c.UI)
168+
if managerErr == nil {
169+
_, vmInventoryErr := os.Stat(manager.InventoryPath())
170+
if vmInventoryErr == nil {
171+
return manager.InventoryPath()
172+
}
173+
}
174+
175+
if _, vagrantInventoryErr := os.Stat(filepath.Join(c.Trellis.Path, VagrantInventoryFilePath)); vagrantInventoryErr == nil {
176+
return VagrantInventoryFilePath
177+
}
178+
179+
return ""
180+
}

cmd/provision_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func TestProvisionRunValidations(t *testing.T) {
7272
func TestProvisionRun(t *testing.T) {
7373
defer trellis.LoadFixtureProject(t)()
7474
trellis := trellis.NewTrellis()
75+
trellis.CliConfig.Vm.Manager = "mock"
7576

7677
cases := []struct {
7778
name string

cmd/vm.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"runtime"
6+
7+
"github.com/mitchellh/cli"
8+
"github.com/roots/trellis-cli/pkg/lima"
9+
"github.com/roots/trellis-cli/pkg/vm"
10+
"github.com/roots/trellis-cli/trellis"
11+
)
12+
13+
func newVmManager(trellis *trellis.Trellis, ui cli.Ui) (manager vm.Manager, err error) {
14+
switch trellis.CliConfig.Vm.Manager {
15+
case "auto":
16+
switch runtime.GOOS {
17+
case "darwin":
18+
return lima.NewManager(trellis, ui)
19+
default:
20+
return nil, fmt.Errorf("No VM managers are supported on %s yet.", runtime.GOOS)
21+
}
22+
case "lima":
23+
return lima.NewManager(trellis, ui)
24+
case "mock":
25+
return vm.NewMockManager(trellis, ui)
26+
}
27+
28+
return nil, fmt.Errorf("VM manager not found")
29+
}

cmd/vm_delete.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package cmd
2+
3+
import (
4+
"flag"
5+
"strings"
6+
7+
"github.com/manifoldco/promptui"
8+
"github.com/mitchellh/cli"
9+
"github.com/posener/complete"
10+
"github.com/roots/trellis-cli/trellis"
11+
)
12+
13+
type VmDeleteCommand struct {
14+
UI cli.Ui
15+
Trellis *trellis.Trellis
16+
flags *flag.FlagSet
17+
force bool
18+
}
19+
20+
func NewVmDeleteCommand(ui cli.Ui, trellis *trellis.Trellis) *VmDeleteCommand {
21+
c := &VmDeleteCommand{UI: ui, Trellis: trellis}
22+
c.init()
23+
return c
24+
}
25+
26+
func (c *VmDeleteCommand) init() {
27+
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
28+
c.flags.Usage = func() { c.UI.Info(c.Help()) }
29+
c.flags.BoolVar(&c.force, "force", false, "Delete VM without confirmation.")
30+
}
31+
32+
func (c *VmDeleteCommand) Run(args []string) int {
33+
if err := c.Trellis.LoadProject(); err != nil {
34+
c.UI.Error(err.Error())
35+
return 1
36+
}
37+
38+
c.Trellis.CheckVirtualenv(c.UI)
39+
40+
if err := c.flags.Parse(args); err != nil {
41+
return 1
42+
}
43+
44+
args = c.flags.Args()
45+
46+
commandArgumentValidator := &CommandArgumentValidator{required: 0, optional: 0}
47+
commandArgumentErr := commandArgumentValidator.validate(args)
48+
if commandArgumentErr != nil {
49+
c.UI.Error(commandArgumentErr.Error())
50+
c.UI.Output(c.Help())
51+
return 1
52+
}
53+
54+
siteName, err := c.Trellis.FindSiteNameFromEnvironment("development", "")
55+
if err != nil {
56+
c.UI.Error(err.Error())
57+
return 1
58+
}
59+
60+
manager, err := newVmManager(c.Trellis, c.UI)
61+
if err != nil {
62+
c.UI.Error("Error: " + err.Error())
63+
return 1
64+
}
65+
66+
if c.force || c.confirmDeletion() {
67+
if err := manager.DeleteInstance(siteName); err != nil {
68+
c.UI.Error("Error: " + err.Error())
69+
return 1
70+
}
71+
}
72+
73+
return 0
74+
}
75+
76+
func (c *VmDeleteCommand) Synopsis() string {
77+
return "Deletes the development virtual machine."
78+
}
79+
80+
func (c *VmDeleteCommand) Help() string {
81+
helpText := `
82+
Usage: trellis vm delete [options]
83+
84+
Deletes the development virtual machine.
85+
VMs must be in a stopped state before they can be deleted.
86+
87+
Delete without prompting for confirmation:
88+
$ trellis vm delete --force
89+
90+
Options:
91+
--force Delete VM without confirmation.
92+
-h, --help Show this help
93+
`
94+
95+
return strings.TrimSpace(helpText)
96+
}
97+
98+
func (c *VmDeleteCommand) AutocompleteFlags() complete.Flags {
99+
return complete.Flags{
100+
"--force": complete.PredictNothing,
101+
}
102+
}
103+
104+
func (c *VmDeleteCommand) confirmDeletion() bool {
105+
prompt := promptui.Prompt{
106+
Label: "Delete virtual machine",
107+
IsConfirm: true,
108+
}
109+
110+
_, err := prompt.Run()
111+
112+
if err != nil {
113+
c.UI.Info("Aborted. Not deleting virtual machine.")
114+
return false
115+
}
116+
117+
return true
118+
}

cmd/vm_delete_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cmd
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/mitchellh/cli"
8+
"github.com/roots/trellis-cli/trellis"
9+
)
10+
11+
func TestVmDeleteRunValidations(t *testing.T) {
12+
defer trellis.LoadFixtureProject(t)()
13+
14+
cases := []struct {
15+
name string
16+
projectDetected bool
17+
args []string
18+
out string
19+
code int
20+
}{
21+
{
22+
"no_project",
23+
false,
24+
nil,
25+
"No Trellis project detected",
26+
1,
27+
},
28+
{
29+
"too_many_args",
30+
true,
31+
[]string{"foo"},
32+
"Error: too many arguments",
33+
1,
34+
},
35+
}
36+
37+
for _, tc := range cases {
38+
t.Run(tc.name, func(t *testing.T) {
39+
ui := cli.NewMockUi()
40+
trellis := trellis.NewMockTrellis(tc.projectDetected)
41+
vmDeleteCommand := NewVmDeleteCommand(ui, trellis)
42+
43+
code := vmDeleteCommand.Run(tc.args)
44+
45+
if code != tc.code {
46+
t.Errorf("expected code %d to be %d", code, tc.code)
47+
}
48+
49+
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
50+
51+
if !strings.Contains(combined, tc.out) {
52+
t.Errorf("expected output %q to contain %q", combined, tc.out)
53+
}
54+
})
55+
}
56+
}

0 commit comments

Comments
 (0)