Skip to content

Add interactive mode to provision #535

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
127 changes: 113 additions & 14 deletions cmd/provision.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cmd

import (
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"strings"

"github.com/mitchellh/cli"
Expand All @@ -19,12 +22,14 @@ func NewProvisionCommand(ui cli.Ui, trellis *trellis.Trellis) *ProvisionCommand
}

type ProvisionCommand struct {
UI cli.Ui
flags *flag.FlagSet
extraVars string
tags string
Trellis *trellis.Trellis
verbose bool
UI cli.Ui
flags *flag.FlagSet
extraVars string
interactive bool
playbookName string
tags string
Trellis *trellis.Trellis
verbose bool
}

func (c *ProvisionCommand) init() {
Expand All @@ -33,6 +38,8 @@ func (c *ProvisionCommand) init() {
c.flags.StringVar(&c.extraVars, "extra-vars", "", "Additional variables which are passed through to Ansible as 'extra-vars'")
c.flags.StringVar(&c.tags, "tags", "", "only run roles and tasks tagged with these values")
c.flags.BoolVar(&c.verbose, "verbose", false, "Enable Ansible's verbose mode")
c.flags.BoolVar(&c.interactive, "interactive", false, "Enable interactive mode to select tags to provision")
c.flags.BoolVar(&c.interactive, "i", false, "Enable interactive mode to select tags to provision")
}

func (c *ProvisionCommand) Run(args []string) int {
Expand Down Expand Up @@ -65,11 +72,15 @@ func (c *ProvisionCommand) Run(args []string) int {
return 1
}

galaxyInstallCommand := &GalaxyInstallCommand{c.UI, c.Trellis}
galaxyInstallCommand.Run([]string{})
c.playbookName = "server.yml"

if environment == "development" {
c.playbookName = "dev.yml"
os.Setenv("ANSIBLE_HOST_KEY_CHECKING", "false")
}

playbook := ansible.Playbook{
Name: "server.yml",
Name: c.playbookName,
Env: environment,
Verbose: c.verbose,
}
Expand All @@ -79,15 +90,41 @@ func (c *ProvisionCommand) Run(args []string) int {
}

if c.tags != "" {
if c.interactive {
c.UI.Error("--interactive and --tags cannot be used together. Please use one or the other.")
}

playbook.AddArg("--tags", c.tags)
}

if c.interactive {
_, err := exec.LookPath("fzf")
if err != nil {
c.UI.Error("No `fzf` command found. fzf is required to use interactive mode.")
}

tags, err := c.getTags()
if err != nil {
c.UI.Error(err.Error())
return 1
}

selectedTags, err := c.selectedTagsFromFzf(tags)
if err != nil {
c.UI.Error(err.Error())
return 1
}

playbook.AddArg("--tags", strings.Join(selectedTags, ","))
}

if environment == "development" {
os.Setenv("ANSIBLE_HOST_KEY_CHECKING", "false")
playbook.SetName("dev.yml")
playbook.SetInventory(findDevInventory(c.Trellis, c.UI))
}

galaxyInstallCommand := &GalaxyInstallCommand{c.UI, c.Trellis}
galaxyInstallCommand.Run([]string{})

provision := command.WithOptions(
command.WithUiOutput(c.UI),
command.WithLogging(c.UI),
Expand Down Expand Up @@ -130,11 +167,16 @@ Provision and provide extra vars to Ansible:

$ trellis provision --extra-vars key=value production

Provision using interactive mode to select tags:

$ trellis provision -i production

Arguments:
ENVIRONMENT Name of environment (ie: production)

Options:
--extra-vars (multiple) Set additional variables as key=value or YAML/JSON, if filename prepend with @
-i, --interactive Enter interactive mode to select tags to provision (requires fzf)
--tags (multiple) Only run roles and tasks tagged with these values
--verbose Enable Ansible's verbose mode
-h, --help Show this help
Expand All @@ -149,8 +191,65 @@ func (c *ProvisionCommand) AutocompleteArgs() complete.Predictor {

func (c *ProvisionCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"--extra-vars": complete.PredictNothing,
"--tags": complete.PredictNothing,
"--verbose": complete.PredictNothing,
"-i": complete.PredictNothing,
"--interactive": complete.PredictNothing,
"--extra-vars": complete.PredictNothing,
"--tags": complete.PredictNothing,
"--verbose": complete.PredictNothing,
}
}

func (c *ProvisionCommand) getTags() ([]string, error) {
tagsPlaybook := ansible.Playbook{
Name: c.playbookName,
Env: c.flags.Arg(0),
Args: []string{"--list-tags"},
}

tagsProvision := command.WithOptions(
command.WithUiOutput(c.UI),
).Cmd("ansible-playbook", tagsPlaybook.CmdArgs())

output := &bytes.Buffer{}
tagsProvision.Stdout = output

if err := tagsProvision.Run(); err != nil {
return nil, err
}

tags := ansible.ParseTags(output.String())

return tags, nil
}

func (c *ProvisionCommand) selectedTagsFromFzf(tags []string) ([]string, error) {
output := &bytes.Buffer{}
input := strings.NewReader(strings.Join(tags, "\n"))

previewCmd := fmt.Sprintf("trellis exec ansible-playbook %s --list-tasks --tags {}", c.playbookName)

fzf := command.WithOptions(command.WithTermOutput()).Cmd(
"fzf",
[]string{
"-m",
"--height", "50%",
"--reverse",
"--border",
"--border-label", "Select tags to provision (use TAB to select multiple tags)",
"--border-label-pos", "5",
"--preview", previewCmd,
"--preview-label", "Tasks for tag",
},
)
fzf.Stdin = input
fzf.Stdout = output

err := fzf.Run()
if err != nil {
return nil, err
}

selectedTags := strings.Split(strings.TrimSpace(output.String()), "\n")

return selectedTags, nil
}
8 changes: 4 additions & 4 deletions pkg/ansible/playbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ type Playbook struct {
Env string
Verbose bool
ExtraVars map[string]string
args []string
Args []string
}

func (p *Playbook) AddArg(name string, value string) *Playbook {
p.args = append(p.args, name+"="+value)
p.Args = append(p.Args, name+"="+value)
return p
}

Expand All @@ -28,7 +28,7 @@ func (p *Playbook) AddExtraVar(name string, value string) *Playbook {
}

func (p *Playbook) AddExtraVars(extraVars string) *Playbook {
p.args = append(p.args, fmt.Sprintf("-e %s", extraVars))
p.Args = append(p.Args, fmt.Sprintf("-e %s", extraVars))
return p
}

Expand All @@ -52,7 +52,7 @@ func (p *Playbook) CmdArgs() []string {
args = append(args, "-vvvv")
}

args = append(args, p.args...)
args = append(args, p.Args...)

if p.Env != "" {
p.AddExtraVar("env", p.Env)
Expand Down
44 changes: 44 additions & 0 deletions pkg/ansible/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package ansible

import (
"regexp"
"sort"
"strings"
)

/*
Parse output from ansible-playbook --list-tags

Example output:

```
playbook: dev.yml

play #1 (web:&development): WordPress Server: Install LEMP Stack with PHP and MariaDB MySQL TAGS: []
TASK TAGS: [common, composer, dotenv, fail2ban, ferm, letsencrypt, logrotate, mail, mailhog, mailpit, mariadb, memcached, nginx, nginx-includes, nginx-sites, ntp, php, sshd, wordpress, wordpress-install, wordpress-install-directories, wordpress-setup, wordpress-setup-database, wordpress-setup-nginx, wordpress-setup-nginx-client-cert, wordpress-setup-self-signed-certificate, wp-cli, xdebug]

```
*/
func ParseTags(output string) []string {
re := regexp.MustCompile(`TASK TAGS:\s*\[([^\]]*)\]`)
match := re.FindStringSubmatch(output)

if len(match) < 2 {
return []string{}
}

// Split by comma and trim each tag
rawTags := strings.Split(match[1], ",")
var tags []string

for _, tag := range rawTags {
trimmed := strings.TrimSpace(tag)

if trimmed != "" {
tags = append(tags, trimmed)
}
}

sort.Strings(tags)
return tags
}
Loading