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

Virtualenv support #55

Merged
merged 13 commits into from
Sep 22, 2019
66 changes: 60 additions & 6 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,72 @@ package cmd

import (
"fmt"
"github.com/mitchellh/cli"
"os"
"os/exec"
"strings"
"syscall"

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

func logCmd(cmd *exec.Cmd, ui cli.Ui, output bool) {
cmd.Stderr = os.Stderr
type ExecCommand struct {
UI cli.Ui
Trellis *trellis.Trellis
}

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

var command string
var cmdArgs []string

switch len(args) {
case 0:
c.UI.Error("Error: missing COMMAND argument\n")
c.UI.Output(c.Help())
return 1
default:
command = args[0]
cmdArgs = args
}

cmdPath, err := exec.LookPath(command)
if err != nil {
c.UI.Error(fmt.Sprintf("Error: %s not found", command))
return 1
}

if output {
cmd.Stdout = &cli.UiWriter{ui}
env := os.Environ()
execErr := syscall.Exec(cmdPath, cmdArgs, env)
if execErr != nil {
c.UI.Error(fmt.Sprintf("Error running %s: %s", args[0], execErr))
return 1
}

fmt.Println("Running command =>", strings.Join(cmd.Args, " "))
return 0
}

func (c *ExecCommand) Synopsis() string {
return "Exec runs a command in the Trellis virtualenv"
}

func (c *ExecCommand) Help() string {
helpText := `
Usage: trellis exec [options]

Exec activates the Trellis virtual environment and executes the command specified.

Run a custom ansible-playbook command:

$ trellis exec ansible-playbook --version

Options:
-h, --help show this help
`

return strings.TrimSpace(helpText)
}
47 changes: 47 additions & 0 deletions cmd/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cmd

import (
"strings"
"testing"

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

func TestExecRunValidations(t *testing.T) {
ui := cli.NewMockUi()

cases := []struct {
name string
projectDetected bool
args []string
out string
code int
}{
{
"no_project",
false,
nil,
"No Trellis project detected",
1,
},
}

for _, tc := range cases {
mockProject := &MockProject{tc.projectDetected}
trellis := trellis.NewTrellis(mockProject)
execCommand := &ExecCommand{ui, trellis}

code := execCommand.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)
}
}
}
110 changes: 110 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cmd

import (
"fmt"
"strings"
"time"

"github.com/briandowns/spinner"
"github.com/mitchellh/cli"
"trellis-cli/trellis"
)

type InitCommand struct {
UI cli.Ui
Trellis *trellis.Trellis
}

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

switch len(args) {
case 0:
default:
c.UI.Error(fmt.Sprintf("Error: too many arguments (expected 0, got %d)\n", len(args)))
c.UI.Output(c.Help())
return 1
}

if err := c.Trellis.CreateConfigDir(); err != nil {
c.UI.Error(err.Error())
return 1
}

if ok, _ := c.Trellis.Virtualenv.Installed(); !ok {
c.UI.Info("virtualenv not found")
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
s.Suffix = " Installing virtualenv..."
s.FinalMSG = "✓ virtualenv installed\n"
s.Start()
c.Trellis.Virtualenv.Install()
s.Stop()
}

c.UI.Info(fmt.Sprintf("Creating virtualenv in %s", c.Trellis.Virtualenv.Path))

err := c.Trellis.Virtualenv.Create()
if err != nil {
c.UI.Error(fmt.Sprintf("Error creating virtualenv: %s", err))
return 1
}

c.UI.Info("✓ Virtualenv created\n")

pip := execCommand("pip", "install", "-r", "requirements.txt")

s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
s.Suffix = " Installing pip dependencies (pip install -r requirements.txt) ..."
s.FinalMSG = "✓ Dependencies installed\n"
s.Start()

err = pip.Run()

if err != nil {
s.Stop()
c.UI.Error(fmt.Sprintf("Error installing pip requirements: %s", err))
return 1
}

s.Stop()

return 0
}

func (c *InitCommand) Synopsis() string {
return "Initializes an existing Trellis project"
}

func (c *InitCommand) Help() string {
helpText := `
Usage: trellis init [options]

Initializes an existing Trellis project to be managed by trellis-cli.
The initialization process does two things:

1. installs virtualenv if necessary (see below for details)
2. creates a virtual environment specific to the project to manage dependencies
3. installs dependencies via pip (specified by requirements.txt in your Trellis project)

trellis-cli will attempt to use an already installed method to manage virtualenvs
and only fallback to installing virtualenv if necessary:

1. if python3 is installed, use built-in virtualenv feature
2. use virtualenv command if available
3. finally install virtualenv at $HOME/.trellis/virtualenv

To learn more about virtual environments, see https://docs.python.org/3/tutorial/venv.html

This command is idempotent meaning it can be run multiple times without side-effects.

$ trellis init

Options:
-h, --help show this help
`

return strings.TrimSpace(helpText)
}
54 changes: 54 additions & 0 deletions cmd/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cmd

import (
"strings"
"testing"

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

func TestInitRunValidations(t *testing.T) {
ui := cli.NewMockUi()

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 {
mockProject := &MockProject{tc.projectDetected}
trellis := trellis.NewTrellis(mockProject)
initCommand := &InitCommand{ui, trellis}

code := initCommand.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)
}
}
}
14 changes: 14 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
package cmd

import (
"fmt"
"github.com/mitchellh/cli"
"os"
"os/exec"
"strings"
)

var execCommand = exec.Command

func logCmd(cmd *exec.Cmd, ui cli.Ui, output bool) {
cmd.Stderr = os.Stderr

if output {
cmd.Stdout = &cli.UiWriter{ui}
}

fmt.Println("Running command =>", strings.Join(cmd.Args, " "))
}
Loading