Skip to content

Commit 9c44d91

Browse files
committed
Add interactive mode to provision
Uses fzf to interactively let you select tags to be used for the provision.
1 parent 1eebc4f commit 9c44d91

File tree

3 files changed

+160
-18
lines changed

3 files changed

+160
-18
lines changed

cmd/provision.go

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"flag"
6+
"fmt"
57
"os"
8+
"os/exec"
69
"strings"
710

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

2124
type ProvisionCommand struct {
22-
UI cli.Ui
23-
flags *flag.FlagSet
24-
extraVars string
25-
tags string
26-
Trellis *trellis.Trellis
27-
verbose bool
25+
UI cli.Ui
26+
flags *flag.FlagSet
27+
extraVars string
28+
interactive bool
29+
playbookName string
30+
tags string
31+
Trellis *trellis.Trellis
32+
verbose bool
2833
}
2934

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

3845
func (c *ProvisionCommand) Run(args []string) int {
@@ -65,11 +72,15 @@ func (c *ProvisionCommand) Run(args []string) int {
6572
return 1
6673
}
6774

68-
galaxyInstallCommand := &GalaxyInstallCommand{c.UI, c.Trellis}
69-
galaxyInstallCommand.Run([]string{})
75+
c.playbookName = "server.yml"
76+
77+
if environment == "development" {
78+
c.playbookName = "dev.yml"
79+
os.Setenv("ANSIBLE_HOST_KEY_CHECKING", "false")
80+
}
7081

7182
playbook := ansible.Playbook{
72-
Name: "server.yml",
83+
Name: c.playbookName,
7384
Env: environment,
7485
Verbose: c.verbose,
7586
}
@@ -79,15 +90,41 @@ func (c *ProvisionCommand) Run(args []string) int {
7990
}
8091

8192
if c.tags != "" {
93+
if c.interactive {
94+
c.UI.Error("--interactive and --tags cannot be used together. Please use one or the other.")
95+
}
96+
8297
playbook.AddArg("--tags", c.tags)
8398
}
8499

100+
if c.interactive {
101+
_, err := exec.LookPath("fzf")
102+
if err != nil {
103+
c.UI.Error("No `fzf` command found. fzf is required to use interactive mode.")
104+
}
105+
106+
tags, err := c.getTags()
107+
if err != nil {
108+
c.UI.Error(err.Error())
109+
return 1
110+
}
111+
112+
selectedTags, err := c.selectedTagsFromFzf(tags)
113+
if err != nil {
114+
c.UI.Error(err.Error())
115+
return 1
116+
}
117+
118+
playbook.AddArg("--tags", strings.Join(selectedTags, ","))
119+
}
120+
85121
if environment == "development" {
86-
os.Setenv("ANSIBLE_HOST_KEY_CHECKING", "false")
87-
playbook.SetName("dev.yml")
88122
playbook.SetInventory(findDevInventory(c.Trellis, c.UI))
89123
}
90124

125+
galaxyInstallCommand := &GalaxyInstallCommand{c.UI, c.Trellis}
126+
galaxyInstallCommand.Run([]string{})
127+
91128
provision := command.WithOptions(
92129
command.WithUiOutput(c.UI),
93130
command.WithLogging(c.UI),
@@ -130,11 +167,16 @@ Provision and provide extra vars to Ansible:
130167
131168
$ trellis provision --extra-vars key=value production
132169
170+
Provision using interactive mode to select tags:
171+
172+
$ trellis provision -i production
173+
133174
Arguments:
134175
ENVIRONMENT Name of environment (ie: production)
135176
136177
Options:
137178
--extra-vars (multiple) Set additional variables as key=value or YAML/JSON, if filename prepend with @
179+
-i, --interactive Enter interactive mode to select tags to provision (requires fzf)
138180
--tags (multiple) Only run roles and tasks tagged with these values
139181
--verbose Enable Ansible's verbose mode
140182
-h, --help Show this help
@@ -149,8 +191,64 @@ func (c *ProvisionCommand) AutocompleteArgs() complete.Predictor {
149191

150192
func (c *ProvisionCommand) AutocompleteFlags() complete.Flags {
151193
return complete.Flags{
152-
"--extra-vars": complete.PredictNothing,
153-
"--tags": complete.PredictNothing,
154-
"--verbose": complete.PredictNothing,
194+
"-i": complete.PredictNothing,
195+
"--interactive": complete.PredictNothing,
196+
"--extra-vars": complete.PredictNothing,
197+
"--tags": complete.PredictNothing,
198+
"--verbose": complete.PredictNothing,
155199
}
156200
}
201+
202+
func (c *ProvisionCommand) getTags() ([]string, error) {
203+
tagsPlaybook := ansible.Playbook{
204+
Name: c.playbookName,
205+
Env: c.flags.Arg(0),
206+
Args: []string{"--list-tags"},
207+
}
208+
209+
tagsProvision := command.WithOptions(
210+
command.WithUiOutput(c.UI),
211+
).Cmd("ansible-playbook", tagsPlaybook.CmdArgs())
212+
213+
output := &bytes.Buffer{}
214+
tagsProvision.Stdout = output
215+
216+
if err := tagsProvision.Run(); err != nil {
217+
return nil, err
218+
}
219+
220+
tags := ansible.ParseTags(output.String())
221+
222+
return tags, nil
223+
}
224+
225+
func (c *ProvisionCommand) selectedTagsFromFzf(tags []string) ([]string, error) {
226+
output := &bytes.Buffer{}
227+
input := strings.NewReader(strings.Join(tags, "\n"))
228+
229+
previewCmd := fmt.Sprintf("trellis exec ansible-playbook %s --list-tasks --tags {}", c.playbookName)
230+
231+
fzf := command.WithOptions(command.WithTermOutput()).Cmd(
232+
"fzf",
233+
[]string{
234+
"-m",
235+
"--height", "50%",
236+
"--reverse",
237+
"--border",
238+
"--header", "Select tags to provision (use TAB to select multiple tags)",
239+
"--header-first",
240+
"--preview", previewCmd,
241+
},
242+
)
243+
fzf.Stdin = input
244+
fzf.Stdout = output
245+
246+
err := fzf.Run()
247+
if err != nil {
248+
return nil, err
249+
}
250+
251+
selectedTags := strings.Split(strings.TrimSpace(output.String()), "\n")
252+
253+
return selectedTags, nil
254+
}

pkg/ansible/playbook.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ type Playbook struct {
1010
Env string
1111
Verbose bool
1212
ExtraVars map[string]string
13-
args []string
13+
Args []string
1414
}
1515

1616
func (p *Playbook) AddArg(name string, value string) *Playbook {
17-
p.args = append(p.args, name+"="+value)
17+
p.Args = append(p.Args, name+"="+value)
1818
return p
1919
}
2020

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

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

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

55-
args = append(args, p.args...)
55+
args = append(args, p.Args...)
5656

5757
if p.Env != "" {
5858
p.AddExtraVar("env", p.Env)

pkg/ansible/tags.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ansible
2+
3+
import (
4+
"regexp"
5+
"sort"
6+
"strings"
7+
)
8+
9+
/*
10+
Parse output from ansible-playbook --list-tags
11+
12+
Example output:
13+
14+
```
15+
playbook: dev.yml
16+
17+
play #1 (web:&development): WordPress Server: Install LEMP Stack with PHP and MariaDB MySQL TAGS: []
18+
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]
19+
20+
```
21+
*/
22+
func ParseTags(output string) []string {
23+
re := regexp.MustCompile(`TASK TAGS:\s*\[([^\]]*)\]`)
24+
match := re.FindStringSubmatch(output)
25+
26+
if len(match) < 2 {
27+
return []string{}
28+
}
29+
30+
// Split by comma and trim each tag
31+
rawTags := strings.Split(match[1], ",")
32+
var tags []string
33+
34+
for _, tag := range rawTags {
35+
trimmed := strings.TrimSpace(tag)
36+
37+
if trimmed != "" {
38+
tags = append(tags, trimmed)
39+
}
40+
}
41+
42+
sort.Strings(tags)
43+
return tags
44+
}

0 commit comments

Comments
 (0)